├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .gitjournal.toml ├── CHANGELOG.md ├── Cargo.toml ├── README.md ├── cliff.toml ├── crates ├── chekov-macros │ ├── Cargo.toml │ └── src │ │ ├── aggregate.rs │ │ ├── command.rs │ │ ├── event.rs │ │ ├── event_handler.rs │ │ └── lib.rs ├── chekov │ ├── Cargo.toml │ └── src │ │ ├── aggregate │ │ ├── instance │ │ │ ├── internal.rs │ │ │ ├── mod.rs │ │ │ └── runtime.rs │ │ ├── mod.rs │ │ ├── registry.rs │ │ └── resolver.rs │ │ ├── application │ │ ├── builder.rs │ │ ├── internal.rs │ │ └── mod.rs │ │ ├── command │ │ ├── consistency.rs │ │ ├── handler │ │ │ ├── instance.rs │ │ │ ├── mod.rs │ │ │ └── registry.rs │ │ ├── metadata.rs │ │ └── mod.rs │ │ ├── error.rs │ │ ├── event │ │ ├── applier.rs │ │ ├── handler.rs │ │ ├── mod.rs │ │ └── resolver.rs │ │ ├── event_store.rs │ │ ├── lib.rs │ │ ├── message.rs │ │ ├── prelude.rs │ │ ├── router.rs │ │ ├── subscriber │ │ ├── listener.rs │ │ ├── manager.rs │ │ ├── mod.rs │ │ └── subscriber.rs │ │ └── tests │ │ ├── aggregates │ │ ├── mod.rs │ │ ├── persistency.rs │ │ ├── runtime.rs │ │ ├── state.rs │ │ ├── subscription.rs │ │ └── support │ │ │ ├── helpers.rs │ │ │ └── mod.rs │ │ └── mod.rs ├── event_store-backend-inmemory │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── event_store-backend-postgres │ ├── Cargo.toml │ └── src │ │ ├── error.rs │ │ ├── lib.rs │ │ └── sql.rs ├── event_store-core │ ├── Cargo.toml │ └── src │ │ ├── backend │ │ ├── error.rs │ │ └── mod.rs │ │ ├── error.rs │ │ ├── event │ │ ├── error.rs │ │ ├── mod.rs │ │ └── test.rs │ │ ├── event_bus │ │ ├── error.rs │ │ └── mod.rs │ │ ├── lib.rs │ │ ├── storage │ │ ├── error.rs │ │ └── mod.rs │ │ ├── stream │ │ ├── error.rs │ │ └── mod.rs │ │ └── versions.rs ├── event_store-eventbus-inmemory │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── event_store-eventbus-postgres │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── event_store-storage-inmemory │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── event_store-storage-postgres │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── event_store │ ├── .env │ ├── .gitignore │ ├── Cargo.toml │ └── src │ │ ├── connection │ │ ├── messaging.rs │ │ └── mod.rs │ │ ├── event │ │ └── mod.rs │ │ ├── event_store │ │ ├── logic.rs │ │ ├── mod.rs │ │ └── runtime.rs │ │ ├── lib.rs │ │ ├── prelude.rs │ │ ├── storage │ │ ├── appender.rs │ │ ├── mod.rs │ │ ├── reader.rs │ │ └── test.rs │ │ └── subscriptions │ │ ├── error.rs │ │ ├── fsm.rs │ │ ├── mod.rs │ │ ├── pub_sub.rs │ │ ├── state.rs │ │ ├── subscriber.rs │ │ ├── subscription.rs │ │ ├── supervisor.rs │ │ └── tests │ │ ├── mod.rs │ │ ├── monitoring_subscription.rs │ │ ├── persistent_fsm.rs │ │ ├── pub_sub.rs │ │ ├── subscribe_to_stream.rs │ │ ├── subscription_acknowledgement.rs │ │ ├── subscription_catch_up.rs │ │ ├── support │ │ ├── event.rs │ │ ├── mod.rs │ │ └── subscriber.rs │ │ └── transient_fsm.rs └── watcher │ ├── Cargo.toml │ └── src │ ├── app.rs │ ├── main.rs │ ├── ui.rs │ └── ui │ └── util.rs ├── examples ├── bank │ ├── .env │ ├── Cargo.toml │ └── src │ │ ├── account.rs │ │ ├── account │ │ ├── aggregate.rs │ │ ├── projector.rs │ │ └── repository.rs │ │ ├── commands.rs │ │ ├── events.rs │ │ ├── http.rs │ │ └── main.rs └── gift_shop │ ├── Cargo.toml │ └── src │ ├── account.rs │ ├── account │ ├── aggregate.rs │ ├── projector.rs │ └── repository.rs │ ├── commands.rs │ ├── events.rs │ ├── gift_card.rs │ ├── http.rs │ ├── main.rs │ └── order.rs ├── scripts └── tests │ ├── docker-compose.yml │ └── postgres │ ├── setup.sh │ ├── setup_bank.sql │ ├── setup_event_store.sql │ ├── setup_gift_shop.sql │ └── test.sql └── sonar-project.properties /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock 4 | bs-config.js 5 | -------------------------------------------------------------------------------- /.gitjournal.toml: -------------------------------------------------------------------------------- 1 | categories = ["Added", "Changed", "Fixed", "Improved", "Removed"] 2 | category_delimiters = ["[", "]"] 3 | colored_output = true 4 | enable_debug = true 5 | excluded_commit_tags = [] 6 | enable_footers = false 7 | show_commit_hash = true 8 | show_prefix = false 9 | sort_by = "date" 10 | template_prefix = "JIRA-1234" 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | ## [unreleased] 6 | 7 | ### Features 8 | 9 | - Improve pubsub management 10 | 11 | ### Miscellaneous Tasks 12 | 13 | - Update CHANGELOG.md for 0.1.0 14 | 15 | 16 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | default-members = ["crates/*"] 4 | members = ["crates/*", "examples/*"] 5 | 6 | [workspace.dependencies] 7 | uuid = { version = "1.3.0", features = ["serde", "v4"] } 8 | serde = { version = "1.0.117", features = ["derive"] } 9 | sqlx = { version = "0.6.2", features = [ 10 | "chrono", 11 | "time", 12 | "uuid", 13 | "json", 14 | "offline", 15 | "runtime-actix-native-tls", 16 | ] } 17 | async-trait = "0.1.51" 18 | serde_json = "1.0.68" 19 | actix = "0.12.0" 20 | futures = "0.3.17" 21 | log = "0.4.14" 22 | tokio = { version = "1.12.0", features = ["full"] } 23 | tracing = "0.1.28" 24 | tracing-futures = "0.2.5" 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Chekov

2 | 3 |
4 | A CQRS/ES framework for building application in Rust 5 |
6 | 7 |
8 | 9 |
10 | 11 | [![Actions Status](https://github.com/freyskeyd/chekov/workflows/CI/badge.svg)](https://github.com/Freyskeyd/chekov/actions) [![Coverage Status](https://coveralls.io/repos/github/Freyskeyd/chekov/badge.svg?branch=master&service=github)](https://coveralls.io/github/Freyskeyd/chekov?branch=master) [![dependency status](https://deps.rs/repo/github/freyskeyd/chekov/status.svg)](https://deps.rs/repo/github/freyskeyd/chekov) [![Crates.io](https://img.shields.io/crates/v/chekov.svg)](https://crates.io/crates/chekov) [![doc.rs](https://docs.rs/chekov/badge.svg)](https://docs.rs/chekov) [![doc-latest](https://img.shields.io/badge/docs-latest-blue.svg?style=flat-square)](https://freyskeyd.github.io/chekov/chekov/) 12 | 13 |
14 | 15 | ## Table of Contents 16 | - [Features](#features) 17 | - [Getting started](#getting-started) 18 | - [FAQ](#faq) 19 | - [Need help?](#need-help) 20 | - [Contributing](#contributing) 21 | 22 | --- 23 | 24 | ## Features 25 | 26 | - `Postgres` EventStore backend 27 | - Dispatch `Command` to `Aggregate` or `CommandHandler` 28 | - Generate `Event` from `Aggregate` and persist them 29 | - Apply `Event` to `Aggregate` 30 | - Store and notify `Event` with subscriptions 31 | - Dispatch `Event` to `EventHandler` 32 | 33 | ## Getting started 34 | 35 | ### Choosing an EventStore backend 36 | 37 | Chekov works only with `Postgres` backend for now (and `InMemory` for test purpose). The choice is easy to make! 38 | 39 | But some more backends need to be implemented, [see the related issue](#14). 40 | 41 | ### Defining Aggregates 42 | 43 | An Aggregate is a struct that hold a domain state. Here's an example of a UserAggregate: 44 | 45 | ```rust 46 | #[derive(Default, Aggregate)] 47 | #[aggregate(identity = "user")] 48 | struct User { 49 | user_id: Option, 50 | account_id: Option, 51 | } 52 | 53 | /// Define an Executor for the `CreateUser` command 54 | /// The result is a list of events in case of success 55 | impl CommandExecutor for User { 56 | fn execute(cmd: CreateUser, _state: &Self) -> Result, CommandExecutorError> { 57 | Ok(vec![UserCreated { 58 | user_id: cmd.user_id, 59 | account_id: cmd.account_id, 60 | }]) 61 | } 62 | } 63 | 64 | /// Define an Applier for the `UserCreated` event 65 | /// Applier is a mutation action on the aggregate 66 | #[chekov::applier] 67 | impl EventApplier for User { 68 | fn apply(&mut self, event: &UserCreated) -> Result<(), ApplyError> { 69 | self.user_id = Some(event.user_id); 70 | self.account_id = Some(event.account_id); 71 | 72 | Ok(()) 73 | } 74 | } 75 | 76 | ``` 77 | 78 | ### Defining Commands 79 | 80 | You need to create a struct per command, any type of struct can implement `Command` but we advise to use struct for a better readability. 81 | 82 | A command can only produce (or not) one type of events and it targets a single Aggregate. 83 | A command must have a single and unique `identifier` that is used to route the command to the right target. 84 | 85 | ```rust 86 | 87 | #[derive(Debug, Command)] 88 | #[command(event = "UserCreated", aggregate = "User")] 89 | struct CreateUser { 90 | #[command(identifier)] 91 | user_id: Uuid, 92 | account_id: Uuid, 93 | } 94 | ``` 95 | 96 | ### Defining Events 97 | 98 | An `Event` can be a `struct` or an `enum`. 99 | 100 | ```rust 101 | #[derive(Event, Deserialize, Serialize)] 102 | struct UserCreated { 103 | user_id: Uuid, 104 | account_id: Uuid, 105 | } 106 | ``` 107 | 108 | ### Defining Saga 109 | 110 | Not implemented yet 111 | 112 | ## FAQ 113 | 114 | ### Does `Chekov` is production ready ? 115 | 116 | No its not. Some critical part of the project are still not implemented and a lot of code needs to be refactored before that. 117 | 118 | ## Need Help? 119 | 120 | Feel free to open issue in case of bugs or features requests. [Discussions](https://github.com/Freyskeyd/chekov/discussions) are also a great starts if you have issue that are not bugs nor features requests. 121 | 122 | ## Contributing 123 | 124 | The project is really early staged and have a lot of pending tasks, one major tasks is to produce a roadmap or some issues that can be used to expose the project vision. Feel free to open a [Discussions](https://github.com/Freyskeyd/chekov/discussions) around it if you want ! 125 | -------------------------------------------------------------------------------- /cliff.toml: -------------------------------------------------------------------------------- 1 | # configuration file for git-cliff (0.1.0) 2 | 3 | [changelog] 4 | # changelog header 5 | header = """ 6 | # Changelog\n 7 | All notable changes to this project will be documented in this file.\n 8 | """ 9 | # template for the changelog body 10 | # https://tera.netlify.app/docs/#introduction 11 | body = """ 12 | {% if version %}\ 13 | ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} 14 | {% else %}\ 15 | ## [unreleased] 16 | {% endif %}\ 17 | {% for group, commits in commits | group_by(attribute="group") %} 18 | ### {{ group | upper_first }} 19 | {% for commit in commits %} 20 | - {% if commit.breaking %}[**breaking**] {% endif %}{{ commit.message | upper_first }}\ 21 | {% endfor %} 22 | {% endfor %}\n 23 | """ 24 | # remove the leading and trailing whitespace from the template 25 | trim = true 26 | # changelog footer 27 | footer = """ 28 | 29 | """ 30 | 31 | [git] 32 | # parse the commits based on https://www.conventionalcommits.org 33 | conventional_commits = true 34 | # filter out the commits that are not conventional 35 | filter_unconventional = true 36 | # regex for parsing and grouping commits 37 | commit_parsers = [ 38 | { message = "^feat", group = "Features"}, 39 | { message = "^fix", group = "Bug Fixes"}, 40 | { message = "^doc", group = "Documentation"}, 41 | { message = "^perf", group = "Performance"}, 42 | { message = "^refactor", group = "Refactor"}, 43 | { message = "^style", group = "Styling"}, 44 | { message = "^test", group = "Testing"}, 45 | { message = "^chore\\(release\\): prepare for", skip = true}, 46 | { message = "^chore", group = "Miscellaneous Tasks"}, 47 | { body = ".*security", group = "Security"}, 48 | ] 49 | # filter out the commits that are not matched by commit parsers 50 | filter_commits = false 51 | # glob pattern for matching git tags 52 | tag_pattern = "v[0-9]*" 53 | # regex for skipping tags 54 | skip_tags = "v0.1.0-beta.1" 55 | # regex for ignoring tags 56 | ignore_tags = "" 57 | # sort the tags chronologically 58 | date_order = false 59 | # sort the commits inside sections by oldest/newest order 60 | sort_commits = "oldest" 61 | -------------------------------------------------------------------------------- /crates/chekov-macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "chekov-macros" 3 | version = "0.1.0" 4 | authors = ["Freyskeyd "] 5 | edition = "2018" 6 | license = "MIT OR Apache-2.0" 7 | repository = "https://github.com/freyskeyd/chekov" 8 | documentation = "https://docs.rs/chekov" 9 | description = "CQRS/ES Framework macros" 10 | keywords = [] 11 | categories = [] 12 | 13 | 14 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 15 | [lib] 16 | proc-macro = true 17 | 18 | [dependencies] 19 | darling = "0.13.0" 20 | quote = "1.0.9" 21 | syn = { version = "1.0.78", features = ["full"] } 22 | proc-macro2 = "1.0.29" 23 | -------------------------------------------------------------------------------- /crates/chekov-macros/src/aggregate.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use darling::*; 4 | use proc_macro2::TokenStream as SynTokenStream; 5 | use quote::*; 6 | use std::result::Result; 7 | use syn::*; 8 | 9 | #[derive(Debug, Clone, FromDeriveInput)] 10 | #[darling(attributes(aggregate), supports(struct_named))] 11 | struct AggregateAttrs { 12 | ident: Ident, 13 | generics: Generics, 14 | 15 | identity: String, 16 | } 17 | 18 | pub fn generate_aggregate( 19 | input: &DeriveInput, 20 | _: &DataStruct, 21 | ) -> Result { 22 | let container: AggregateAttrs = match FromDeriveInput::from_derive_input(input) { 23 | Ok(v) => v, 24 | Err(e) => return Err(e.write_errors()), 25 | }; 26 | 27 | let struct_name = container.ident; 28 | let identity = container.identity; 29 | 30 | let aggregate_event_resolver = 31 | format_ident!("{}EventResolverRegistry", struct_name.to_string()); 32 | 33 | let aggregate_static_resolver = format_ident!( 34 | "{}_STATIC_EVENT_RESOLVER", 35 | struct_name.to_string().to_uppercase() 36 | ); 37 | 38 | Ok(quote! { 39 | 40 | pub struct #aggregate_event_resolver { 41 | names: Vec<&'static str>, 42 | type_id: std::any::TypeId, 43 | applier: chekov::aggregate::resolver::EventApplierFn<#struct_name> 44 | } 45 | 46 | impl chekov::aggregate::resolver::EventResolverItem<#struct_name> for #aggregate_event_resolver { 47 | fn get_names(&self) -> &[&'static str] { 48 | self.names.as_ref() 49 | } 50 | } 51 | 52 | chekov::inventory::collect!(#aggregate_event_resolver); 53 | 54 | chekov::lazy_static! { 55 | #[derive(Clone, Debug)] 56 | static ref #aggregate_static_resolver: chekov::aggregate::resolver::EventResolverRegistry<#struct_name> = { 57 | let mut appliers = std::collections::BTreeMap::new(); 58 | let mut names = std::collections::BTreeMap::new(); 59 | 60 | for registered in chekov::inventory::iter::<#aggregate_event_resolver> { 61 | appliers.insert(registered.type_id, registered.applier); 62 | 63 | registered.names.iter().for_each(|name|{ 64 | names.insert(name.clone(), registered.type_id); 65 | }); 66 | } 67 | 68 | chekov::aggregate::resolver::EventResolverRegistry { 69 | names, 70 | appliers 71 | } 72 | }; 73 | } 74 | 75 | impl chekov::Aggregate for #struct_name { 76 | fn get_event_resolver() -> &'static chekov::aggregate::resolver::EventResolverRegistry { 77 | &*#aggregate_static_resolver 78 | } 79 | 80 | fn identity() -> &'static str { 81 | #identity 82 | } 83 | } 84 | }) 85 | } 86 | -------------------------------------------------------------------------------- /crates/chekov-macros/src/command.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use darling::*; 4 | use proc_macro2::TokenStream as SynTokenStream; 5 | use quote::*; 6 | use std::result::Result; 7 | use syn::*; 8 | 9 | #[derive(Debug, Clone, FromDeriveInput)] 10 | #[darling(attributes(command), supports(struct_named))] 11 | struct CommandAttrs { 12 | ident: Ident, 13 | generics: Generics, 14 | 15 | event: Path, 16 | aggregate: Path, 17 | #[darling(default = "default_handler")] 18 | handler: Path, 19 | #[darling(default)] 20 | identifier: Option, 21 | } 22 | 23 | fn default_handler() -> Path { 24 | syn::Path::from_string("::chekov::prelude::NoHandler").unwrap() 25 | } 26 | 27 | #[derive(Debug, Clone, FromDeriveInput)] 28 | #[darling(attributes(command), supports(struct_named))] 29 | struct CommandHandler { 30 | ident: Ident, 31 | generics: Generics, 32 | } 33 | 34 | #[derive(Debug, Clone, FromField)] 35 | #[darling(attributes(command), forward_attrs(doc))] 36 | struct CommandFieldAttrs { 37 | ident: Option, 38 | attrs: Vec, 39 | 40 | #[darling(default)] 41 | identifier: Option, 42 | } 43 | 44 | pub fn generate_command( 45 | input: &DeriveInput, 46 | data: &DataStruct, 47 | ) -> Result { 48 | let container: CommandAttrs = match FromDeriveInput::from_derive_input(input) { 49 | Ok(v) => v, 50 | Err(e) => return Err(e.write_errors()), 51 | }; 52 | 53 | let struct_name = container.ident; 54 | let event = container.event; 55 | let executor = container.aggregate.get_ident().expect("No Executor"); 56 | let handler = container.handler; 57 | let attrs = data 58 | .fields 59 | .iter() 60 | .filter_map(|i| { 61 | let v: Option = match FromField::from_field(i) { 62 | Ok(v) => Some(v), 63 | Err(_) => None, 64 | }; 65 | 66 | v 67 | }) 68 | .collect::>(); 69 | 70 | let identifiers: Vec<&CommandFieldAttrs> = 71 | attrs.iter().filter(|x| x.identifier.is_some()).collect(); 72 | 73 | if identifiers.len() > 1 { 74 | panic!("Can't use multiple identifier on fields, use identifier on struct instead."); 75 | } 76 | 77 | let identifier_value = identifiers.first().unwrap().ident.clone(); 78 | Ok(quote! { 79 | 80 | impl chekov::Command for #struct_name { 81 | type Event = #event; 82 | type Executor = #executor; 83 | type ExecutorRegistry = ::chekov::aggregate::AggregateInstanceRegistry<#executor>; 84 | type CommandHandler = #handler; 85 | 86 | fn identifier(&self) -> ::std::string::String { 87 | self.#identifier_value.to_string() 88 | } 89 | } 90 | }) 91 | } 92 | 93 | pub fn generate_command_handler( 94 | input: &DeriveInput, 95 | _data: &DataStruct, 96 | ) -> Result { 97 | let container: CommandHandler = match FromDeriveInput::from_derive_input(input) { 98 | Ok(v) => v, 99 | Err(e) => return Err(e.write_errors()), 100 | }; 101 | 102 | let struct_name = container.ident; 103 | 104 | Ok(quote! { 105 | 106 | impl chekov::CommandHandler for #struct_name { 107 | 108 | } 109 | impl actix::SystemService for #struct_name {} 110 | impl actix::Supervised for #struct_name {} 111 | impl actix::Actor for #struct_name { 112 | type Context = actix::Context; 113 | } 114 | // impl chekov::CommandHandler for #struct_name { 115 | // type Event = #event; 116 | // type Executor = #executor; 117 | // type ExecutorRegistry = ::chekov::aggregate::AggregateInstanceRegistry<#executor>; 118 | 119 | // fn identifier(&self) -> ::std::string::String { 120 | // self.#identifier_value.to_string() 121 | // } 122 | // } 123 | }) 124 | } 125 | -------------------------------------------------------------------------------- /crates/chekov-macros/src/event_handler.rs: -------------------------------------------------------------------------------- 1 | use darling::*; 2 | use proc_macro2::TokenStream as SynTokenStream; 3 | use quote::*; 4 | use std::result::Result; 5 | use syn::*; 6 | 7 | #[derive(Debug, Clone, FromDeriveInput)] 8 | #[darling(attributes(aggregate), supports(struct_named))] 9 | struct EventHandlerAttrs { 10 | ident: Ident, 11 | #[allow(dead_code)] 12 | generics: Generics, 13 | } 14 | 15 | pub fn generate_event_handler( 16 | input: &DeriveInput, 17 | _: &DataStruct, 18 | ) -> Result { 19 | let container: EventHandlerAttrs = match FromDeriveInput::from_derive_input(input) { 20 | Ok(v) => v, 21 | Err(e) => return Err(e.write_errors()), 22 | }; 23 | 24 | let struct_name = container.ident; 25 | 26 | let aggregate_event_resolver = 27 | format_ident!("{}EventHandlerEventRegistry", struct_name.to_string(),); 28 | 29 | let aggregate_static_resolver = format_ident!( 30 | "{}_STATIC_EVENT_HANDLER_EVENT_RESOLVER", 31 | struct_name.to_string().to_uppercase() 32 | ); 33 | 34 | Ok(quote! { 35 | 36 | pub struct #aggregate_event_resolver { 37 | names: Vec<&'static str>, 38 | type_id: std::any::TypeId, 39 | handler: fn(&mut #struct_name, chekov::RecordedEvent) -> BoxFuture> 40 | } 41 | 42 | chekov::inventory::collect!(#aggregate_event_resolver); 43 | 44 | chekov::lazy_static! { 45 | #[derive(Clone, Debug)] 46 | static ref #aggregate_static_resolver: chekov::event::resolver::EventHandlerResolverRegistry<#struct_name> = { 47 | let mut handlers = std::collections::BTreeMap::new(); 48 | let mut names = std::collections::BTreeMap::new(); 49 | 50 | for registered in chekov::inventory::iter::<#aggregate_event_resolver> { 51 | handlers.insert(registered.type_id, registered.handler); 52 | 53 | registered.names.iter().for_each(|name|{ 54 | names.insert(name.clone(), registered.type_id); 55 | }); 56 | } 57 | 58 | chekov::event::resolver::EventHandlerResolverRegistry { 59 | names, 60 | handlers 61 | } 62 | }; 63 | } 64 | 65 | #[chekov::async_trait::async_trait] 66 | impl chekov::EventHandler for #struct_name { 67 | 68 | async fn handle_recorded_event(state: &mut Self, event: chekov::RecordedEvent) -> Result<(), chekov::error::HandleError> { 69 | if let Some(resolver) = #aggregate_static_resolver.get(&event.event_type) { 70 | return (resolver)(state, event).await; 71 | } 72 | 73 | Ok(()) 74 | } 75 | } 76 | }) 77 | } 78 | -------------------------------------------------------------------------------- /crates/chekov/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "chekov" 3 | version = "0.1.1" 4 | license = "MIT OR Apache-2.0" 5 | readme = "README.md" 6 | repository = "https://github.com/freyskeyd/chekov" 7 | documentation = "https://docs.rs/chekov" 8 | description = "CQRS/ES Framework" 9 | edition = "2018" 10 | keywords = [] 11 | categories = [] 12 | authors = ["Freyskeyd "] 13 | 14 | [dependencies] 15 | uuid.workspace = true 16 | serde.workspace = true 17 | sqlx.workspace = true 18 | async-trait.workspace = true 19 | serde_json.workspace = true 20 | actix.workspace = true 21 | futures.workspace = true 22 | log.workspace = true 23 | tokio.workspace = true 24 | tracing.workspace = true 25 | tracing-futures.workspace = true 26 | 27 | event_store = { version = "0.1", path = "../event_store" } 28 | chekov-macros = { version = "0.1", path = "../chekov-macros" } 29 | fnv = "1.0.7" 30 | chrono = "0.4.19" 31 | # Until next minor 32 | inventory = "0.1.10" 33 | lazy_static = "1.4.0" 34 | typetag = "0.1.7" 35 | thiserror = "1.0" 36 | 37 | [dev-dependencies] 38 | pretty_assertions = "1.0.0" 39 | test-log = { version = "0.2.8", default-features = false, features = ["trace"] } 40 | test-span = "0.1.1" 41 | tracing-subscriber = { version = "0.3", default-features = false, features = [ 42 | "env-filter", 43 | "fmt", 44 | ] } 45 | 46 | -------------------------------------------------------------------------------- /crates/chekov/src/aggregate/instance/internal.rs: -------------------------------------------------------------------------------- 1 | pub struct CommandExecutionResult { 2 | pub events: Vec, 3 | pub new_version: u64, 4 | pub state: A, 5 | } 6 | -------------------------------------------------------------------------------- /crates/chekov/src/aggregate/resolver.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use std::{any::TypeId, collections::BTreeMap}; 3 | 4 | pub trait EventResolverItem { 5 | fn get_names(&self) -> &[&'static str]; 6 | } 7 | 8 | pub type EventApplierFn = fn(&mut A, RecordedEvent) -> std::result::Result<(), ApplyError>; 9 | 10 | pub struct EventResolverRegistry { 11 | pub names: BTreeMap<&'static str, TypeId>, 12 | pub appliers: BTreeMap>, 13 | } 14 | 15 | impl EventResolverRegistry { 16 | pub fn get_applier(&self, event_name: &str) -> Option<&EventApplierFn> { 17 | let type_id = self.names.get(event_name)?; 18 | 19 | self.appliers.get(type_id) 20 | } 21 | } 22 | 23 | #[cfg(test)] 24 | mod test { 25 | use crate::tests::aggregates::support::ExampleAggregate; 26 | 27 | use super::*; 28 | 29 | #[test] 30 | fn appliers_can_be_fetched() { 31 | let resolver = ExampleAggregate::get_event_resolver(); 32 | let result = resolver.get_applier("MyEvent"); 33 | 34 | assert!(result.is_some()); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /crates/chekov/src/application/builder.rs: -------------------------------------------------------------------------------- 1 | use crate::event::handler::EventHandlerBuilder; 2 | use crate::event::EventHandler; 3 | use crate::message::StartListening; 4 | use crate::Application; 5 | use crate::Router; 6 | use crate::SubscriberManager; 7 | use actix::Actor; 8 | use actix::SystemService; 9 | use event_store::core::storage::Storage; 10 | use futures::Future; 11 | use std::marker::PhantomData; 12 | use std::pin::Pin; 13 | use tracing::trace; 14 | 15 | use super::InternalApplication; 16 | 17 | #[async_trait::async_trait] 18 | pub trait StorageConfig 19 | where 20 | S: Storage, 21 | { 22 | async fn build(&self) -> S; 23 | } 24 | 25 | /// Struct to configure and launch an `Application` instance 26 | /// 27 | /// An application can only be launch though an ApplicationBuilder. 28 | /// 29 | /// This builder will have the responsability to configure every part of the application. 30 | /// 31 | /// Note: In most cases you will not have to use `ApplicationBuilder` directly. 32 | /// `Application` will be the root of everything. 33 | pub struct ApplicationBuilder { 34 | app: std::marker::PhantomData, 35 | storage: Pin>>, 36 | event_handlers: Vec>>>, 37 | listener_url: Option, 38 | } 39 | 40 | impl std::default::Default for ApplicationBuilder { 41 | fn default() -> Self { 42 | Self { 43 | listener_url: None, 44 | event_handlers: Vec::new(), 45 | app: std::marker::PhantomData, 46 | storage: Box::pin(async { A::Storage::default() }), 47 | } 48 | } 49 | } 50 | 51 | impl ApplicationBuilder 52 | where 53 | A: Application, 54 | { 55 | pub fn listener_url(mut self, url: String) -> Self { 56 | self.listener_url = Some(url); 57 | 58 | self 59 | } 60 | 61 | /// Adds an EventHandler to the application 62 | pub fn event_handler(mut self, handler: E) -> Self { 63 | self.event_handlers 64 | .push(Box::pin(EventHandlerBuilder::new(handler).register::())); 65 | self 66 | } 67 | 68 | /// Adds a StorageConfig use later to create the event_store Storage 69 | pub fn with_storage_config + 'static>( 70 | mut self, 71 | storage: CFG, 72 | ) -> Self { 73 | self.storage = Box::pin(async move { storage.build().await }); 74 | 75 | self 76 | } 77 | 78 | /// Adds a preconfigured Backend as Storage. 79 | /// The storage isn't start, only when the application is launched. 80 | pub fn storage> + 'static, E: std::fmt::Debug>( 81 | mut self, 82 | storage: F, 83 | ) -> Self { 84 | self.storage = 85 | Box::pin(async move { storage.await.expect("Unable to connect the storage") }); 86 | 87 | self 88 | } 89 | 90 | /// Launch the application 91 | /// 92 | /// Meaning that: 93 | /// - The storage future is resolved and registered 94 | /// - An EventStore instance is started based on this storage 95 | /// - A Router is started to handle commands 96 | /// - A SubscriberManager is started to dispatch incomming events 97 | #[tracing::instrument(name = "Chekov::Launch", skip(self), fields(app = %A::get_name()))] 98 | pub async fn launch(self) { 99 | trace!( 100 | "Launching a new Chekov instance with {}", 101 | std::any::type_name::() 102 | ); 103 | 104 | let storage: A::Storage = self.storage.await; 105 | 106 | let event_store: event_store::EventStore = event_store::EventStore::builder() 107 | .storage(storage) 108 | .build() 109 | .await 110 | .unwrap(); 111 | 112 | let subscriber_manager_addr = SubscriberManager::::new(self.listener_url).start(); 113 | 114 | ::actix::SystemRegistry::set(subscriber_manager_addr); 115 | 116 | use futures::future::join_all; 117 | 118 | join_all(self.event_handlers).await; 119 | 120 | let event_store_addr = event_store.start(); 121 | 122 | let router = Router:: { 123 | _app: std::marker::PhantomData, 124 | _before_dispatch: vec![], 125 | }; 126 | 127 | let addr = router.start(); 128 | let event_store = crate::event_store::EventStore:: { 129 | addr: event_store_addr, 130 | } 131 | .start(); 132 | 133 | SubscriberManager::::from_registry().do_send(StartListening); 134 | 135 | ::actix::SystemRegistry::set(event_store); 136 | ::actix::SystemRegistry::set(addr); 137 | ::actix::SystemRegistry::set( 138 | InternalApplication:: { 139 | _phantom: PhantomData, 140 | } 141 | .start(), 142 | ); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /crates/chekov/src/application/internal.rs: -------------------------------------------------------------------------------- 1 | use std::marker::PhantomData; 2 | 3 | use crate::Application; 4 | use actix::prelude::*; 5 | 6 | pub(crate) struct InternalApplication { 7 | pub(crate) _phantom: PhantomData, 8 | } 9 | 10 | impl std::default::Default for InternalApplication 11 | where 12 | A: Application, 13 | { 14 | fn default() -> Self { 15 | unimplemented!() 16 | } 17 | } 18 | 19 | impl actix::Actor for InternalApplication 20 | where 21 | A: Application, 22 | { 23 | type Context = Context; 24 | } 25 | 26 | impl actix::SystemService for InternalApplication where A: Application {} 27 | impl actix::Supervised for InternalApplication where A: Application {} 28 | -------------------------------------------------------------------------------- /crates/chekov/src/application/mod.rs: -------------------------------------------------------------------------------- 1 | //! Struct and Trait correlated to Application 2 | 3 | mod builder; 4 | mod internal; 5 | 6 | pub use builder::ApplicationBuilder; 7 | pub(crate) use internal::InternalApplication; 8 | 9 | /// Application are high order logical seperator. 10 | /// 11 | /// A chekov application is a simple entity that will represent a single and unique domain 12 | /// application. 13 | /// 14 | /// It is used to separate and allow multiple chekov application on a single runtime. 15 | /// 16 | /// It's also used to define the typology of the application, like which storage will be used or 17 | /// how to resolve the eventbus's event 18 | /// 19 | /// The Application isn't instanciate at all and it's useless to define fields on it. It just act 20 | /// as a type holder for the entier system. 21 | /// 22 | /// ```rust 23 | /// #[derive(Default)] 24 | /// struct DefaultApp {} 25 | /// 26 | /// impl chekov::Application for DefaultApp { 27 | /// // Define that this application will use a PostgresStorage as event_store 28 | /// type Storage = event_store::prelude::PostgresStorage; 29 | /// } 30 | /// ``` 31 | pub trait Application: Unpin + 'static + Send + std::default::Default { 32 | /// The type of storage used by the application 33 | type Storage: event_store::core::storage::Storage; 34 | 35 | /// Used to initiate the launch of the application 36 | /// 37 | /// It will just return an ApplicationBuilder which will take care of everything. 38 | fn with_default() -> ApplicationBuilder { 39 | ApplicationBuilder::::default() 40 | } 41 | 42 | /// Returns the logical name of the application 43 | /// Mostly used for logs and debugging. 44 | /// 45 | /// You can configure it or use the default value which is the struct full qualified name. 46 | fn get_name() -> &'static str { 47 | std::any::type_name::() 48 | } 49 | } 50 | 51 | #[cfg(test)] 52 | mod tests { 53 | use super::*; 54 | 55 | use crate::tests::aggregates::support::MyApplication; 56 | 57 | #[test] 58 | fn application_must_have_a_name() { 59 | assert_eq!( 60 | MyApplication::get_name(), 61 | "chekov::tests::aggregates::support::MyApplication" 62 | ); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /crates/chekov/src/command/consistency.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, Clone)] 2 | pub enum Consistency { 3 | Strong, 4 | Eventual, 5 | } 6 | -------------------------------------------------------------------------------- /crates/chekov/src/command/handler/instance.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use crate::command::{CommandExecutor, Handler}; 4 | use crate::{ 5 | command::CommandHandler, message::DispatchWithState, prelude::CommandExecutorError, Aggregate, 6 | Application, Command, 7 | }; 8 | use actix::prelude::Handler as ActixHandler; 9 | use actix::prelude::*; 10 | use futures::{FutureExt, TryFutureExt}; 11 | 12 | type DispatchResult = ResponseFuture, A), CommandExecutorError>>; 13 | 14 | #[derive(Default)] 15 | pub struct CommandHandlerInstance { 16 | pub(crate) inner: H, 17 | } 18 | 19 | impl SystemService for CommandHandlerInstance {} 20 | impl Supervised for CommandHandlerInstance {} 21 | impl Actor for CommandHandlerInstance { 22 | type Context = Context; 23 | } 24 | 25 | impl 26 | ActixHandler> for CommandHandlerInstance 27 | where 28 | H: Handler, 29 | A: CommandExecutor, 30 | { 31 | type Result = DispatchResult; 32 | fn handle( 33 | &mut self, 34 | cmd: DispatchWithState, 35 | _ctx: &mut Self::Context, 36 | ) -> Self::Result { 37 | let command = cmd.command; 38 | let state = cmd.state; 39 | let fut = self.inner.handle(command, state.clone()); 40 | Box::pin( 41 | tokio::time::timeout(Duration::from_secs(5), fut) 42 | .map(|res| match res { 43 | Ok(response) => response.map(|events| (events, state)), 44 | Err(error) => { 45 | tracing::error!("First : {:?}", error); 46 | Err(error.into()) 47 | } 48 | }) 49 | .map_err(|error| { 50 | tracing::error!("{:?}", error); 51 | error 52 | }), 53 | ) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /crates/chekov/src/command/handler/mod.rs: -------------------------------------------------------------------------------- 1 | use actix::Context; 2 | use futures::future::BoxFuture; 3 | 4 | use crate::aggregate::StaticState; 5 | use crate::command::Handler; 6 | use crate::prelude::CommandExecutor; 7 | use crate::prelude::CommandExecutorError; 8 | use crate::Command; 9 | 10 | use super::CommandHandler; 11 | 12 | pub(crate) mod instance; 13 | mod registry; 14 | 15 | #[derive(Default)] 16 | pub struct NoHandler {} 17 | 18 | impl CommandHandler for NoHandler {} 19 | 20 | impl + Sync> Handler for NoHandler { 21 | fn handle( 22 | &mut self, 23 | _: C, 24 | _: StaticState, 25 | ) -> BoxFuture<'static, Result, CommandExecutorError>> { 26 | unimplemented!() 27 | } 28 | } 29 | 30 | impl actix::SystemService for NoHandler {} 31 | impl actix::Supervised for NoHandler {} 32 | impl actix::Actor for NoHandler { 33 | type Context = Context; 34 | } 35 | -------------------------------------------------------------------------------- /crates/chekov/src/command/handler/registry.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /crates/chekov/src/command/metadata.rs: -------------------------------------------------------------------------------- 1 | use super::Consistency; 2 | use uuid::Uuid; 3 | 4 | #[derive(Debug, Clone)] 5 | pub struct CommandMetadatas { 6 | pub command_id: Uuid, 7 | pub correlation_id: Uuid, 8 | pub causation_id: Option, 9 | pub consistency: Consistency, 10 | } 11 | 12 | impl std::default::Default for CommandMetadatas { 13 | fn default() -> Self { 14 | Self { 15 | command_id: uuid::Uuid::new_v4(), 16 | correlation_id: uuid::Uuid::new_v4(), 17 | causation_id: None, 18 | consistency: Consistency::Eventual, 19 | } 20 | } 21 | } 22 | 23 | #[cfg(test)] 24 | mod tests { 25 | use super::*; 26 | 27 | #[test] 28 | fn metadatas_can_be_initialize() { 29 | let _meta = CommandMetadatas { 30 | command_id: Uuid::new_v4(), 31 | correlation_id: Uuid::new_v4(), 32 | causation_id: None, 33 | consistency: Consistency::Eventual, 34 | }; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /crates/chekov/src/command/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::aggregate::StaticState; 2 | use crate::event::Event; 3 | use crate::Aggregate; 4 | use crate::CommandExecutorError; 5 | use crate::{event::*, message::Dispatch, Application}; 6 | use actix::SystemService; 7 | 8 | mod consistency; 9 | mod handler; 10 | mod metadata; 11 | 12 | pub use consistency::Consistency; 13 | use futures::future::BoxFuture; 14 | pub(crate) use handler::instance::CommandHandlerInstance; 15 | pub use handler::NoHandler; 16 | pub use metadata::CommandMetadatas; 17 | 18 | /// Define a Command which can be dispatch 19 | pub trait Command: Send + 'static { 20 | /// The Event that can be generated for this command 21 | type Event: Event + event_store::Event; 22 | 23 | /// The Executor that will execute the command and produce the events 24 | /// 25 | /// Note that for now, onlu Aggregate can be used as Executor. 26 | type Executor: CommandExecutor + EventApplier; 27 | 28 | /// The registry where the command will be dispatched 29 | type ExecutorRegistry: SystemService; 30 | 31 | type CommandHandler: CommandHandler + Handler; 32 | 33 | /// Returns the identifier for this command. 34 | /// 35 | /// The identifier is used to choose the right executor. 36 | fn identifier(&self) -> String; 37 | } 38 | 39 | #[doc(hidden)] 40 | #[async_trait::async_trait] 41 | pub trait Dispatchable 42 | where 43 | C: Command, 44 | A: Application, 45 | { 46 | async fn dispatch(&self, cmd: C) -> Result, CommandExecutorError> 47 | where 48 | ::ExecutorRegistry: actix::Handler>; 49 | } 50 | 51 | /// Receives a command and an immutable Executor and optionally returns events 52 | pub trait CommandHandler: std::marker::Unpin + Default + 'static + SystemService {} 53 | 54 | pub trait Handler> { 55 | fn handle( 56 | &mut self, 57 | command: C, 58 | state: StaticState, 59 | ) -> BoxFuture<'static, ExecutionResult>; 60 | } 61 | 62 | /// Receives a command and an immutable State and optionally returns events 63 | pub trait CommandExecutor: Aggregate { 64 | fn execute(cmd: T, state: &Self) -> ExecutionResult; 65 | } 66 | 67 | pub type ExecutionResult = std::result::Result, CommandExecutorError>; 68 | -------------------------------------------------------------------------------- /crates/chekov/src/error.rs: -------------------------------------------------------------------------------- 1 | use event_store::{core::error::BoxDynError, prelude::EventStoreError}; 2 | use thiserror::Error; 3 | use tokio::time::error::Elapsed; 4 | 5 | /// Error returns by a CommandExecutor 6 | #[derive(Error, Debug)] 7 | pub enum CommandExecutorError { 8 | #[error(transparent)] 9 | ApplyError(ApplyError), 10 | #[error("Internal router error")] 11 | InternalRouterError, 12 | #[error(transparent)] 13 | ExecutionError(BoxDynError), 14 | #[error(transparent)] 15 | EventStoreError(EventStoreError), 16 | #[error("Command execution timedout {0}")] 17 | Timedout(Elapsed), 18 | } 19 | 20 | impl std::convert::From for CommandExecutorError { 21 | fn from(_: actix::MailboxError) -> Self { 22 | Self::InternalRouterError 23 | } 24 | } 25 | 26 | impl std::convert::From for CommandExecutorError { 27 | fn from(error: EventStoreError) -> Self { 28 | Self::EventStoreError(error) 29 | } 30 | } 31 | 32 | impl std::convert::From for CommandExecutorError { 33 | fn from(error: ApplyError) -> Self { 34 | Self::ApplyError(error) 35 | } 36 | } 37 | 38 | impl From for CommandExecutorError { 39 | fn from(error: Elapsed) -> Self { 40 | Self::Timedout(error) 41 | } 42 | } 43 | 44 | /// Error returns by a failling EventApplier 45 | #[derive(Error, Debug)] 46 | pub enum ApplyError { 47 | #[error("Unknown")] 48 | Any, 49 | } 50 | 51 | #[derive(Error, Debug)] 52 | pub enum HandleError { 53 | #[error("Unknown")] 54 | Any, 55 | } 56 | -------------------------------------------------------------------------------- /crates/chekov/src/event/applier.rs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Freyskeyd/chekov/ac9966da79db26f4b1342107132c68c1cfd34dfe/crates/chekov/src/event/applier.rs -------------------------------------------------------------------------------- /crates/chekov/src/event/mod.rs: -------------------------------------------------------------------------------- 1 | //! Struct and Trait correlated to Event 2 | use crate::error::{ApplyError, HandleError}; 3 | use event_store::prelude::RecordedEvent; 4 | use futures::future::BoxFuture; 5 | use std::any::TypeId; 6 | use std::collections::BTreeMap; 7 | 8 | pub(crate) mod handler; 9 | 10 | #[doc(hidden)] 11 | pub mod resolver; 12 | 13 | pub use handler::EventHandler; 14 | #[doc(hidden)] 15 | pub use handler::EventHandlerInstance; 16 | 17 | /// Define an Event which can be produced and consumed 18 | // pub trait Event: event_store::prelude::Event { 19 | pub trait Event: Send + Clone {} 20 | 21 | /// Define an event applier 22 | pub trait EventApplier { 23 | fn apply(&mut self, event: &E) -> Result<(), ApplyError>; 24 | } 25 | 26 | /// Receive an immutable event to handle 27 | pub trait Handler { 28 | fn handle(&mut self, event: &E) -> BoxFuture>; 29 | } 30 | -------------------------------------------------------------------------------- /crates/chekov/src/event/resolver.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use crate::error::HandleError; 3 | 4 | #[doc(hidden)] 5 | pub type EventHandlerFn = fn(&mut A, RecordedEvent) -> BoxFuture>; 6 | 7 | #[doc(hidden)] 8 | pub struct EventHandlerResolverRegistry { 9 | pub names: BTreeMap<&'static str, TypeId>, 10 | pub handlers: BTreeMap>, 11 | } 12 | 13 | impl EventHandlerResolverRegistry { 14 | pub fn get(&self, event_name: &str) -> Option<&EventHandlerFn> { 15 | let type_id = self.names.get(event_name)?; 16 | 17 | self.handlers.get(type_id) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /crates/chekov/src/event_store.rs: -------------------------------------------------------------------------------- 1 | use crate::application::Application; 2 | use crate::message::{ 3 | ExecuteAppender, ExecuteReader, ExecuteStreamForward, ExecuteStreamInfo, GetEventStoreAddr, 4 | }; 5 | pub use ::event_store::prelude::Event; 6 | pub use ::event_store::prelude::RecordedEvent; 7 | use actix::{Addr, Context, Handler, MailboxError, ResponseFuture, SystemService}; 8 | use event_store::prelude::Stream; 9 | use event_store::prelude::{EventStoreError, StreamForward}; 10 | use futures::TryFutureExt; 11 | use std::marker::PhantomData; 12 | use std::pin::Pin; 13 | use uuid::Uuid; 14 | 15 | pub use event_store::prelude::PostgresEventBus; 16 | pub use event_store::storage::PostgresStorage; 17 | 18 | pub(crate) struct EventStore { 19 | pub(crate) addr: Addr>, 20 | } 21 | 22 | impl EventStore 23 | where 24 | A: Application, 25 | { 26 | pub async fn with_appender( 27 | appender: event_store::prelude::Appender, 28 | ) -> Result, EventStoreError>, MailboxError> { 29 | Self::from_registry().send(ExecuteAppender(appender)).await 30 | } 31 | 32 | pub async fn with_reader( 33 | reader: event_store::prelude::Reader, 34 | ) -> Result, EventStoreError>, MailboxError> { 35 | Self::from_registry().send(ExecuteReader(reader)).await 36 | } 37 | 38 | pub async fn stream_forward( 39 | stream_uuid: String, 40 | ) -> Result< 41 | Result< 42 | Pin< 43 | Box, EventStoreError>> + Send>, 44 | >, 45 | EventStoreError, 46 | >, 47 | MailboxError, 48 | > { 49 | Self::from_registry() 50 | .send(ExecuteStreamForward(stream_uuid)) 51 | .await 52 | } 53 | 54 | #[allow(dead_code)] 55 | pub async fn stream_info( 56 | stream_uuid: &str, 57 | ) -> Result, MailboxError> { 58 | Self::from_registry() 59 | .send(ExecuteStreamInfo(stream_uuid.to_string())) 60 | .await 61 | } 62 | 63 | pub async fn get_addr() -> Result>, MailboxError> { 64 | Self::from_registry() 65 | .send(GetEventStoreAddr { 66 | _phantom: PhantomData, 67 | }) 68 | .await 69 | } 70 | } 71 | 72 | impl Handler> for EventStore 73 | where 74 | A: Application, 75 | { 76 | type Result = Addr>; 77 | 78 | fn handle(&mut self, _: GetEventStoreAddr, _: &mut Self::Context) -> Self::Result { 79 | self.addr.clone() 80 | } 81 | } 82 | 83 | impl Handler for EventStore 84 | where 85 | A: Application, 86 | { 87 | type Result = ResponseFuture< 88 | Result< 89 | Pin< 90 | Box, EventStoreError>> + Send>, 91 | >, 92 | EventStoreError, 93 | >, 94 | >; 95 | 96 | fn handle(&mut self, reader: ExecuteStreamForward, _: &mut Self::Context) -> Self::Result { 97 | let addr = self.addr.clone(); 98 | Box::pin(async move { 99 | addr.send(StreamForward { 100 | correlation_id: Uuid::new_v4(), 101 | stream_uuid: reader.0, 102 | }) 103 | .await? 104 | .map(|res| res.stream) 105 | }) 106 | } 107 | } 108 | 109 | impl Handler for EventStore 110 | where 111 | A: Application, 112 | { 113 | type Result = ResponseFuture, EventStoreError>>; 114 | 115 | fn handle(&mut self, reader: ExecuteReader, _: &mut Self::Context) -> Self::Result { 116 | let addr = self.addr.clone(); 117 | Box::pin(reader.0.execute(addr)) 118 | } 119 | } 120 | 121 | impl Handler for EventStore 122 | where 123 | A: Application, 124 | { 125 | type Result = ResponseFuture, EventStoreError>>; 126 | 127 | fn handle(&mut self, appender: ExecuteAppender, _: &mut Self::Context) -> Self::Result { 128 | let addr = self.addr.clone(); 129 | Box::pin(appender.0.execute(addr)) 130 | } 131 | } 132 | 133 | impl Handler for EventStore 134 | where 135 | A: Application, 136 | { 137 | type Result = ResponseFuture>; 138 | 139 | fn handle(&mut self, appender: ExecuteStreamInfo, _: &mut Self::Context) -> Self::Result { 140 | let addr = self.addr.clone(); 141 | 142 | Box::pin( 143 | addr.send(event_store::prelude::StreamInfo { 144 | correlation_id: uuid::Uuid::new_v4(), 145 | stream_uuid: appender.0, 146 | }) 147 | .map_ok_or_else(|e| Err(e.into()), |r| r), 148 | ) 149 | } 150 | } 151 | 152 | impl actix::Actor for EventStore 153 | where 154 | A: Application, 155 | { 156 | type Context = Context; 157 | } 158 | 159 | impl actix::SystemService for EventStore where A: Application {} 160 | impl actix::Supervised for EventStore where A: Application {} 161 | impl std::default::Default for EventStore 162 | where 163 | A: Application, 164 | { 165 | fn default() -> Self { 166 | unimplemented!() 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /crates/chekov/src/message.rs: -------------------------------------------------------------------------------- 1 | use std::marker::PhantomData; 2 | 3 | use super::Application; 4 | use super::Command; 5 | use super::CommandExecutorError; 6 | use crate::command::CommandMetadatas; 7 | use crate::Aggregate; 8 | use crate::Event; 9 | use actix::prelude::*; 10 | use event_store::core::storage::Storage; 11 | use event_store::prelude::*; 12 | use uuid::Uuid; 13 | 14 | #[derive(Debug, Clone, Message)] 15 | #[rtype(result = "Result, CommandExecutorError>")] 16 | pub struct Dispatch { 17 | pub metadatas: CommandMetadatas, 18 | pub storage: std::marker::PhantomData, 19 | pub command: C, 20 | } 21 | 22 | #[derive(Debug, Clone, Message)] 23 | #[rtype(result = "Result<(Vec, S), CommandExecutorError>")] 24 | pub struct DispatchWithState { 25 | pub metadatas: CommandMetadatas, 26 | pub storage: std::marker::PhantomData, 27 | pub command: C, 28 | pub state: S, 29 | } 30 | 31 | impl DispatchWithState { 32 | pub(crate) fn from_dispatch(dispatch: Dispatch, state: S) -> Self { 33 | Self { 34 | metadatas: dispatch.metadatas, 35 | storage: dispatch.storage, 36 | command: dispatch.command, 37 | state, 38 | } 39 | } 40 | } 41 | 42 | #[derive(Debug, Clone)] 43 | pub struct EventMetadatas { 44 | pub correlation_id: Option, 45 | pub causation_id: Option, 46 | pub stream_name: String, 47 | } 48 | 49 | #[doc(hidden)] 50 | #[derive(Debug, Clone, Message)] 51 | #[rtype(result = "Result<(), ()>")] 52 | pub struct EventEnvelope { 53 | pub event: E, 54 | pub meta: EventMetadatas, 55 | } 56 | 57 | #[doc(hidden)] 58 | #[derive(Debug, Clone, Message)] 59 | #[rtype(result = "Result<(), ()>")] 60 | pub struct ResolveAndApply(pub RecordedEvent); 61 | 62 | #[doc(hidden)] 63 | #[derive(Debug, Clone, Message)] 64 | #[rtype(result = "Result<(), ()>")] 65 | pub struct ResolveAndApplyMany(pub Vec); 66 | 67 | #[derive(Message)] 68 | #[rtype(result = "Result, event_store::prelude::EventStoreError>")] 69 | pub(crate) struct ExecuteReader(pub(crate) event_store::prelude::Reader); 70 | 71 | #[derive(Message)] 72 | #[rtype(result = "Result< 73 | std::pin::Pin< 74 | Box, EventStoreError>> + Send>, 75 | >, 76 | EventStoreError, 77 | >")] 78 | pub(crate) struct ExecuteStreamForward(pub(crate) String); 79 | 80 | #[derive(Message)] 81 | #[rtype(result = "Result, event_store::prelude::EventStoreError>")] 82 | pub(crate) struct ExecuteAppender(pub(crate) event_store::prelude::Appender); 83 | 84 | #[derive(Message)] 85 | #[rtype(result = "Result")] 86 | pub(crate) struct ExecuteStreamInfo(pub(crate) String); 87 | 88 | #[derive(Message)] 89 | #[rtype(result = "u64")] 90 | pub(crate) struct AggregateVersion; 91 | 92 | #[derive(Message)] 93 | #[rtype(result = "A")] 94 | pub(crate) struct AggregateState(pub(crate) PhantomData); 95 | 96 | #[derive(Message, Debug)] 97 | #[rtype(result = "()")] 98 | pub(crate) struct StartListening; 99 | 100 | #[derive(Message)] 101 | #[rtype(result = "Addr>")] 102 | pub(crate) struct GetEventStoreAddr { 103 | pub(crate) _phantom: PhantomData, 104 | } 105 | 106 | #[derive(Message)] 107 | #[rtype(result = "Option>>")] 108 | pub(crate) struct GetAggregateAddr { 109 | pub(crate) identifier: String, 110 | pub(crate) _phantom: PhantomData, 111 | } 112 | 113 | #[derive(Message)] 114 | #[rtype(result = "Result>, ()>")] 115 | pub(crate) struct StartAggregate { 116 | pub(crate) identifier: String, 117 | pub(crate) correlation_id: Option, 118 | pub(crate) _aggregate: PhantomData, 119 | pub(crate) _application: PhantomData, 120 | } 121 | 122 | #[derive(Message)] 123 | #[rtype(result = "Result<(), ()>")] 124 | pub(crate) struct ShutdownAggregate { 125 | pub(crate) identifier: String, 126 | } 127 | -------------------------------------------------------------------------------- /crates/chekov/src/prelude.rs: -------------------------------------------------------------------------------- 1 | pub use crate::aggregate::Aggregate; 2 | pub use crate::aggregate::AggregateInstance; 3 | pub use crate::aggregate::AggregateInstanceRegistry; 4 | pub use crate::application::Application; 5 | pub use crate::command::Command; 6 | pub use crate::command::CommandExecutor; 7 | pub use crate::command::CommandHandler; 8 | pub use crate::command::CommandMetadatas; 9 | pub use crate::command::Dispatchable; 10 | pub use crate::command::ExecutionResult; 11 | pub use crate::command::NoHandler; 12 | pub use crate::error::{ApplyError, CommandExecutorError}; 13 | pub use crate::event::handler::EventHandler; 14 | pub use crate::event::handler::Subscribe; 15 | pub use crate::event::EventApplier; 16 | pub use crate::event_store::PostgresEventBus; 17 | pub use crate::event_store::{PostgresStorage, RecordedEvent}; 18 | pub use crate::message::EventEnvelope; 19 | pub use crate::message::EventMetadatas; 20 | pub use crate::router::Router; 21 | pub use chekov_macros::Aggregate; 22 | pub use chekov_macros::CommandHandler; 23 | -------------------------------------------------------------------------------- /crates/chekov/src/router.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | command::Command, command::CommandMetadatas, message::Dispatch, Application, 3 | CommandExecutorError, 4 | }; 5 | use actix::{ResponseFuture, SystemService}; 6 | use futures::TryFutureExt; 7 | use tracing::trace; 8 | 9 | #[doc(hidden)] 10 | #[derive(Default)] 11 | pub struct Router { 12 | pub(crate) _app: std::marker::PhantomData, 13 | pub(crate) _before_dispatch: Vec, 14 | } 15 | 16 | impl ::actix::Actor for Router { 17 | type Context = ::actix::Context; 18 | } 19 | 20 | impl ::actix::registry::SystemService for Router {} 21 | impl ::actix::Supervised for Router {} 22 | 23 | impl ::actix::Handler> for Router 24 | where 25 | ::ExecutorRegistry: actix::Handler>, 26 | { 27 | type Result = ResponseFuture, CommandExecutorError>>; 28 | 29 | #[tracing::instrument(name = "Router", skip(self, _ctx, msg), fields(correlation_id = %msg.metadatas.correlation_id))] 30 | fn handle(&mut self, msg: Dispatch, _ctx: &mut Self::Context) -> Self::Result { 31 | let to = 32 | ::from_registry().recipient::>(); 33 | trace!( 34 | to = ::std::any::type_name::(), 35 | "Route command", 36 | ); 37 | Box::pin(to.send(msg).map_ok_or_else(|e| Err(e.into()), |r| r)) 38 | } 39 | } 40 | 41 | impl Router { 42 | #[tracing::instrument(name = "Dispatcher", skip(cmd, metadatas), fields(correlation_id = %metadatas.correlation_id))] 43 | pub async fn dispatch( 44 | cmd: C, 45 | metadatas: CommandMetadatas, 46 | ) -> Result, CommandExecutorError> 47 | where 48 | ::ExecutorRegistry: actix::Handler>, 49 | { 50 | trace!( 51 | executor = ::std::any::type_name::(), 52 | "Sending {} to Router", 53 | ::std::any::type_name::() 54 | ); 55 | 56 | Self::from_registry() 57 | .send(Dispatch:: { 58 | metadatas, 59 | storage: std::marker::PhantomData, 60 | command: cmd, 61 | }) 62 | .await? 63 | } 64 | } 65 | 66 | #[cfg(test)] 67 | mod tests { 68 | use super::*; 69 | use crate::tests::aggregates::support::{InvalidCommand, MyApplication}; 70 | use uuid::Uuid; 71 | 72 | #[actix::test] 73 | #[ignore] 74 | async fn can_route_a_command() { 75 | // TODO: Improve this tests with a particular APP 76 | let _ = Router::::dispatch( 77 | InvalidCommand(Uuid::new_v4()), 78 | CommandMetadatas::default(), 79 | ) 80 | .await; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /crates/chekov/src/subscriber/listener.rs: -------------------------------------------------------------------------------- 1 | use super::EventNotification; 2 | use crate::{Application, SubscriberManager}; 3 | use actix::ActorContext; 4 | use actix::{Actor, Addr, AsyncContext, Context}; 5 | use sqlx::postgres::PgNotification; 6 | use std::convert::TryFrom; 7 | use std::marker::PhantomData; 8 | use tracing::trace; 9 | 10 | pub struct Listener { 11 | _phantom: std::marker::PhantomData, 12 | pub manager: Addr>, 13 | pub listening: String, 14 | } 15 | 16 | impl actix::Actor for Listener { 17 | type Context = Context; 18 | 19 | #[tracing::instrument(name = "Listener", skip(self, _ctx), fields(app = %A::get_name()))] 20 | fn started(&mut self, _ctx: &mut Self::Context) { 21 | trace!("Created a Listener instance"); 22 | } 23 | } 24 | 25 | impl Listener { 26 | #[tracing::instrument(name = "Listener", skip(url, manager), fields(app = %A::get_name()))] 27 | pub async fn setup(url: String, manager: Addr>) -> Result, ()> { 28 | let mut listener = sqlx::postgres::PgListener::connect(&url).await.unwrap(); 29 | listener.listen("events").await.unwrap(); 30 | trace!("Starting listener with {}", url); 31 | 32 | Ok(Listener::create(move |ctx| { 33 | ctx.add_stream(listener.into_stream()); 34 | 35 | Listener { 36 | _phantom: PhantomData, 37 | manager, 38 | listening: url, 39 | } 40 | })) 41 | } 42 | } 43 | 44 | impl actix::StreamHandler> for Listener { 45 | fn handle(&mut self, item: Result, _ctx: &mut Self::Context) { 46 | if let Ok(m) = item { 47 | if let Ok(event) = EventNotification::try_from(m.payload()) { 48 | self.manager.do_send(event); 49 | } 50 | } 51 | } 52 | 53 | fn finished(&mut self, ctx: &mut Self::Context) { 54 | ctx.stop(); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /crates/chekov/src/subscriber/mod.rs: -------------------------------------------------------------------------------- 1 | use actix::prelude::*; 2 | use sqlx::postgres::PgNotification; 3 | use std::{convert::TryFrom, str::FromStr}; 4 | 5 | mod listener; 6 | mod manager; 7 | mod subscriber; 8 | 9 | pub use listener::Listener; 10 | pub use manager::SubscriberManager; 11 | pub use subscriber::Subscriber; 12 | 13 | #[derive(Debug, Message)] 14 | #[rtype(result = "()")] 15 | struct Notif(PgNotification); 16 | 17 | #[derive(Clone, Debug, Message)] 18 | #[rtype(result = "()")] 19 | pub struct EventNotification { 20 | #[allow(dead_code)] 21 | stream_id: i32, 22 | stream_uuid: String, 23 | // FIXME: use u64 24 | first_stream_version: i32, 25 | last_stream_version: i32, 26 | } 27 | 28 | struct NonEmptyString(String); 29 | 30 | impl FromStr for NonEmptyString { 31 | type Err = &'static str; 32 | 33 | fn from_str(s: &str) -> Result { 34 | if s.is_empty() { 35 | Err("unable to parse stream_uuid") 36 | } else { 37 | Ok(NonEmptyString(s.to_owned())) 38 | } 39 | } 40 | } 41 | 42 | impl From for String { 43 | fn from(s: NonEmptyString) -> Self { 44 | s.0 45 | } 46 | } 47 | 48 | impl<'a> TryFrom<&'a str> for EventNotification { 49 | type Error = &'static str; 50 | 51 | fn try_from(value: &'a str) -> Result { 52 | let mut through = value.splitn(4, ','); 53 | 54 | let stream_uuid = through 55 | .next() 56 | .ok_or("unable to parse stream_uuid")? 57 | .parse::()? 58 | .into(); 59 | 60 | let stream_id = through 61 | .next() 62 | .ok_or("unable to parse stream_id")? 63 | .parse::() 64 | .or(Err("unable to convert stream_id to i32"))?; 65 | 66 | let first_stream_version = through 67 | .next() 68 | .ok_or("unable to parse first_stream_version")? 69 | .parse::() 70 | .or(Err("unable to convert first_stream_version to i32"))?; 71 | 72 | let last_stream_version = through 73 | .next() 74 | .ok_or("unable to parse last_stream_version")? 75 | .parse::() 76 | .or(Err("unable to convert last_stream_version to i32"))?; 77 | 78 | Ok(Self { 79 | stream_uuid, 80 | stream_id, 81 | first_stream_version, 82 | last_stream_version, 83 | }) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /crates/chekov/src/subscriber/subscriber.rs: -------------------------------------------------------------------------------- 1 | use super::EventNotification; 2 | use actix::{Actor, Context, Handler}; 3 | 4 | pub struct Subscriber { 5 | _stream: String, 6 | } 7 | 8 | impl Actor for Subscriber { 9 | type Context = Context; 10 | fn started(&mut self, _ctx: &mut Self::Context) {} 11 | } 12 | 13 | impl Handler for Subscriber { 14 | type Result = (); 15 | 16 | fn handle(&mut self, _: EventNotification, _ctx: &mut Self::Context) -> Self::Result {} 17 | } 18 | -------------------------------------------------------------------------------- /crates/chekov/src/tests/aggregates/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod support; 2 | 3 | mod persistency; 4 | mod runtime; 5 | mod state; 6 | mod subscription; 7 | -------------------------------------------------------------------------------- /crates/chekov/src/tests/aggregates/persistency.rs: -------------------------------------------------------------------------------- 1 | use super::support::*; 2 | use crate::event_store::EventStore; 3 | use crate::prelude::*; 4 | use crate::{assert_aggregate_state, assert_aggregate_version}; 5 | use event_store::prelude::Reader; 6 | use test_log::test; 7 | use uuid::Uuid; 8 | 9 | #[test(actix::test)] 10 | async fn should_persist_pending_events_in_order_applied() -> Result<(), Box> 11 | { 12 | let identifier = Uuid::new_v4(); 13 | start_application().await; 14 | let _ = start_aggregate(&identifier).await; 15 | 16 | let result = AggregateInstanceRegistry::::execute::( 17 | AppendItem(10, identifier), 18 | ) 19 | .await; 20 | 21 | assert!(result.is_ok()); 22 | 23 | let events = result.unwrap(); 24 | assert!(events.len() == 10); 25 | 26 | let reader = Reader::default().stream(&identifier)?.limit(100); 27 | let recorded_events = EventStore::::with_reader(reader).await??; 28 | 29 | assert!(recorded_events.len() == 10); 30 | 31 | let identifier_as_string = identifier.to_string(); 32 | 33 | let slice = recorded_events 34 | .into_iter() 35 | .filter(|event| event.stream_uuid == identifier_as_string) 36 | .map(|event| serde_json::from_value::(event.data).unwrap()) 37 | .collect::>(); 38 | 39 | assert!(slice == (1..=10).collect::>()); 40 | 41 | Ok(()) 42 | } 43 | 44 | #[test(actix::test)] 45 | async fn should_not_persist_events_when_command_returns_no_events( 46 | ) -> Result<(), Box> { 47 | let identifier = Uuid::new_v4(); 48 | start_application().await; 49 | let _ = start_aggregate(&identifier).await; 50 | 51 | let result = AggregateInstanceRegistry::::execute::( 52 | AppendItem(0, identifier), 53 | ) 54 | .await; 55 | 56 | assert!(result.is_ok()); 57 | 58 | let events = result.unwrap(); 59 | assert!(events.is_empty()); 60 | 61 | let reader = Reader::default().stream(&identifier)?.limit(100); 62 | let recorded_events = EventStore::::with_reader(reader).await??; 63 | 64 | assert!(recorded_events.is_empty()); 65 | 66 | Ok(()) 67 | } 68 | 69 | #[test(actix::test)] 70 | async fn should_persist_event_metadata() -> Result<(), Box> { 71 | // TODO: Implement metadata persistency 72 | Ok(()) 73 | } 74 | 75 | #[test(actix::test)] 76 | async fn should_reload_persisted_events_when_restarting_aggregate_process( 77 | ) -> Result<(), Box> { 78 | let identifier = Uuid::new_v4(); 79 | start_application().await; 80 | let addr = start_aggregate(&identifier).await; 81 | 82 | let result = AggregateInstanceRegistry::::execute::( 83 | AppendItem(10, identifier), 84 | ) 85 | .await; 86 | 87 | assert!(result.is_ok()); 88 | 89 | let events = result.unwrap(); 90 | assert!(events.len() == 10); 91 | 92 | let reader = Reader::default().stream(&identifier)?.limit(100); 93 | let recorded_events = EventStore::::with_reader(reader).await??; 94 | 95 | assert!(recorded_events.len() == 10); 96 | 97 | let res = 98 | AggregateInstanceRegistry::::shutdown_aggregate(identifier.to_string()) 99 | .await; 100 | 101 | assert!(res.is_ok(), "Aggregate couldn't be shutdown"); 102 | assert!(!addr.connected()); 103 | 104 | let addr_after = start_aggregate(&identifier).await; 105 | 106 | assert_aggregate_version!(&addr_after, 10); 107 | assert_aggregate_state!( 108 | &addr_after, 109 | ExampleAggregate { 110 | items: (1..=10).collect(), 111 | last_index: 9 112 | } 113 | ); 114 | 115 | Ok(()) 116 | } 117 | 118 | #[test(actix::test)] 119 | async fn should_reload_persisted_events_in_batches_when_restarting_aggregate_process( 120 | ) -> Result<(), Box> { 121 | let identifier = Uuid::new_v4(); 122 | start_application().await; 123 | let addr = start_aggregate(&identifier).await; 124 | 125 | let result = AggregateInstanceRegistry::::execute::( 126 | AppendItem(300, identifier), 127 | ) 128 | .await; 129 | 130 | assert!(result.is_ok()); 131 | 132 | let events = result.unwrap(); 133 | assert!(events.len() == 300); 134 | 135 | let res = 136 | AggregateInstanceRegistry::::shutdown_aggregate(identifier.to_string()) 137 | .await; 138 | 139 | assert!(res.is_ok(), "Aggregate couldn't be shutdown"); 140 | assert!(!addr.connected()); 141 | 142 | let addr_after = start_aggregate(&identifier).await; 143 | 144 | assert_aggregate_version!(&addr_after, 300); 145 | assert_aggregate_state!( 146 | &addr_after, 147 | ExampleAggregate { 148 | items: (1..=300).collect(), 149 | last_index: 299 150 | } 151 | ); 152 | 153 | Ok(()) 154 | } 155 | -------------------------------------------------------------------------------- /crates/chekov/src/tests/aggregates/runtime.rs: -------------------------------------------------------------------------------- 1 | use super::support::*; 2 | use crate::assert_aggregate_version; 3 | use crate::message::Dispatch; 4 | use crate::prelude::*; 5 | use std::marker::PhantomData; 6 | use test_log::test; 7 | use uuid::Uuid; 8 | 9 | #[test(actix::test)] 10 | async fn should_be_able_to_start() -> Result<(), Box> { 11 | let identifier = Uuid::new_v4(); 12 | start_application().await; 13 | let instance = start_aggregate(&identifier).await; 14 | 15 | assert_aggregate_version!(instance, 0); 16 | 17 | let _ = instance 18 | .send(Dispatch::<_, MyApplication> { 19 | metadatas: CommandMetadatas::default(), 20 | storage: PhantomData, 21 | command: AppendItem(1, identifier), 22 | }) 23 | .await; 24 | 25 | assert_aggregate_version!(instance, 1); 26 | 27 | Ok(()) 28 | } 29 | 30 | #[actix::test] 31 | async fn can_recover_from_fail_execution() -> Result<(), Box> { 32 | let instance = AggregateInstance { 33 | inner: ExampleAggregate::default(), 34 | current_version: 1, 35 | resolver: ExampleAggregate::get_event_resolver(), 36 | identity: String::new(), 37 | }; 38 | 39 | let result = AggregateInstance::execute( 40 | instance.create_mutable_state(), 41 | Dispatch::<_, MyApplication> { 42 | storage: PhantomData, 43 | command: InvalidCommand(Uuid::new_v4()), 44 | metadatas: CommandMetadatas::default(), 45 | }, 46 | ) 47 | .await; 48 | 49 | assert!(matches!(result, Err(_))); 50 | 51 | let result = AggregateInstance::execute( 52 | instance.create_mutable_state(), 53 | Dispatch::<_, MyApplication> { 54 | storage: PhantomData, 55 | command: ValidCommand(Uuid::new_v4()), 56 | metadatas: CommandMetadatas::default(), 57 | }, 58 | ) 59 | .await; 60 | 61 | assert!(matches!(result, Ok(_))); 62 | Ok(()) 63 | } 64 | 65 | #[test] 66 | fn can_apply_event() { 67 | let instance = AggregateInstance { 68 | inner: ExampleAggregate::default(), 69 | current_version: 1, 70 | identity: String::new(), 71 | resolver: ExampleAggregate::get_event_resolver(), 72 | }; 73 | 74 | let result = AggregateInstance::directly_apply( 75 | &mut instance.create_mutable_state(), 76 | &MyEvent { id: Uuid::new_v4() }, 77 | ); 78 | 79 | assert!(matches!(result, Ok(_))); 80 | } 81 | 82 | #[test(actix::test)] 83 | async fn can_execute_a_command() { 84 | let instance = AggregateInstance { 85 | inner: ExampleAggregate::default(), 86 | current_version: 0, 87 | resolver: ExampleAggregate::get_event_resolver(), 88 | identity: String::new(), 89 | }; 90 | let id = Uuid::new_v4(); 91 | 92 | let result: Result<_, CommandExecutorError> = AggregateInstance::execute( 93 | instance.create_mutable_state(), 94 | Dispatch::<_, MyApplication> { 95 | storage: PhantomData, 96 | command: ValidCommand(id), 97 | metadatas: CommandMetadatas::default(), 98 | }, 99 | ) 100 | .await 101 | .map(|(value, _)| value); 102 | 103 | let expected = vec![MyEvent { id }]; 104 | 105 | assert!(matches!(result, Ok(events) if events == expected)); 106 | } 107 | -------------------------------------------------------------------------------- /crates/chekov/src/tests/aggregates/state.rs: -------------------------------------------------------------------------------- 1 | use super::support::*; 2 | use crate::assert_aggregate_version; 3 | use crate::event_store::EventStore; 4 | use crate::message::Dispatch; 5 | use crate::prelude::*; 6 | use event_store::prelude::Appender; 7 | use std::marker::PhantomData; 8 | use test_log::test; 9 | use uuid::Uuid; 10 | 11 | #[actix::test] 12 | async fn should_rebuild_his_state_from_previously_append_events( 13 | ) -> Result<(), Box> { 14 | start_application().await; 15 | 16 | let identifier = Uuid::new_v4(); 17 | let _ = EventStore::::with_appender( 18 | Appender::default() 19 | .event(&ItemAppended(1)) 20 | .unwrap() 21 | .to(&identifier) 22 | .unwrap(), 23 | ) 24 | .await; 25 | 26 | let instance = start_aggregate(&identifier).await; 27 | 28 | assert_aggregate_version!(instance, 1); 29 | 30 | let result = instance 31 | .send(Dispatch::<_, MyApplication> { 32 | metadatas: CommandMetadatas::default(), 33 | storage: PhantomData, 34 | command: AppendItem(1, identifier), 35 | }) 36 | .await; 37 | 38 | assert!(result.is_ok()); 39 | 40 | assert_aggregate_version!(instance, 2); 41 | 42 | Ok(()) 43 | } 44 | 45 | #[actix::test] 46 | async fn should_can_fetch_existing_state() -> Result<(), Box> { 47 | start_application().await; 48 | let identifier = Uuid::new_v4(); 49 | let _ = EventStore::::with_appender( 50 | Appender::default() 51 | .event(&MyEvent { id: identifier }) 52 | .unwrap() 53 | .to(&identifier) 54 | .unwrap(), 55 | ) 56 | .await; 57 | 58 | let result = AggregateInstance::::fetch_existing_state::( 59 | identifier.to_string(), 60 | Uuid::new_v4(), 61 | ) 62 | .await; 63 | 64 | assert_eq!(result.expect("shouldn't fail").len(), 1); 65 | Ok(()) 66 | } 67 | 68 | #[test] 69 | fn can_duplicate_state() { 70 | let instance = AggregateInstance { 71 | inner: ExampleAggregate::default(), 72 | current_version: 0, 73 | resolver: ExampleAggregate::get_event_resolver(), 74 | identity: String::new(), 75 | }; 76 | 77 | let _: ExampleAggregate = instance.create_mutable_state(); 78 | } 79 | -------------------------------------------------------------------------------- /crates/chekov/src/tests/aggregates/subscription.rs: -------------------------------------------------------------------------------- 1 | use crate::{assert_aggregate_state, assert_aggregate_version, message::ResolveAndApply}; 2 | 3 | use super::support::*; 4 | use event_store::{prelude::RecordedEvent, Event, PubSub}; 5 | use test_log::test; 6 | use uuid::Uuid; 7 | 8 | #[test(actix::test)] 9 | async fn aggregate_should_starts_a_pubsub_subscription() { 10 | let identity = Uuid::new_v4(); 11 | let _ = start_context(&identity).await; 12 | 13 | assert_eq!( 14 | 1, 15 | PubSub::has_subscriber_for(identity.to_string()) 16 | .await 17 | .unwrap_or_else(|_| panic!( 18 | "Failed to fetch the subscriber list for the stream {}", 19 | identity 20 | )) 21 | ); 22 | } 23 | 24 | /// Append event directly to aggregate stream 25 | #[test(actix::test)] 26 | async fn should_notify_aggregate_and_mutate_its_state() -> Result<(), Box> { 27 | let identity = Uuid::new_v4(); 28 | let addr = start_context(&identity).await; 29 | 30 | assert_aggregate_version!(&addr, 0); 31 | assert_aggregate_state!( 32 | &addr, 33 | ExampleAggregate { 34 | items: vec![], 35 | last_index: 0 36 | } 37 | ); 38 | 39 | Ok(()) 40 | } 41 | 42 | #[test(actix::test)] 43 | async fn should_ignore_already_seen_events() -> Result<(), Box> { 44 | let identity = Uuid::new_v4(); 45 | let addr = start_context(&identity).await; 46 | 47 | let base = ItemAppended(1); 48 | let event = RecordedEvent { 49 | event_number: 1, 50 | event_uuid: Uuid::new_v4(), 51 | stream_uuid: identity.to_string(), 52 | stream_version: Some(1), 53 | causation_id: None, 54 | correlation_id: None, 55 | event_type: base.event_type().to_string(), 56 | data: serde_json::to_value(base).unwrap(), 57 | metadata: None, 58 | created_at: chrono::offset::Utc::now(), 59 | }; 60 | 61 | assert_aggregate_version!(&addr, 0); 62 | assert_aggregate_state!( 63 | &addr, 64 | ExampleAggregate { 65 | items: vec![], 66 | last_index: 0 67 | } 68 | ); 69 | 70 | let _ = addr.send(ResolveAndApply(event.clone())).await?; 71 | 72 | assert_aggregate_version!(&addr, 1); 73 | assert_aggregate_state!( 74 | &addr, 75 | ExampleAggregate { 76 | items: vec![1], 77 | last_index: 0 78 | } 79 | ); 80 | 81 | let _ = addr.send(ResolveAndApply(event.clone())).await?; 82 | 83 | assert_aggregate_version!(&addr, 1); 84 | assert_aggregate_state!( 85 | &addr, 86 | ExampleAggregate { 87 | items: vec![1], 88 | last_index: 0 89 | } 90 | ); 91 | 92 | let second = ItemAppended(2); 93 | let event2 = RecordedEvent { 94 | event_number: 2, 95 | event_uuid: Uuid::new_v4(), 96 | stream_uuid: identity.to_string(), 97 | stream_version: Some(2), 98 | causation_id: None, 99 | correlation_id: None, 100 | event_type: second.event_type().to_string(), 101 | data: serde_json::to_value(second).unwrap(), 102 | metadata: None, 103 | created_at: chrono::offset::Utc::now(), 104 | }; 105 | 106 | assert!(addr.send(ResolveAndApply(event2.clone())).await?.is_ok()); 107 | 108 | assert_aggregate_version!(&addr, 2); 109 | assert_aggregate_state!( 110 | &addr, 111 | ExampleAggregate { 112 | items: vec![1, 2], 113 | last_index: 1 114 | } 115 | ); 116 | 117 | assert!(addr.send(ResolveAndApply(event)).await?.is_ok()); 118 | 119 | assert_aggregate_version!(&addr, 2); 120 | assert_aggregate_state!( 121 | &addr, 122 | ExampleAggregate { 123 | items: vec![1, 2], 124 | last_index: 1 125 | } 126 | ); 127 | 128 | Ok(()) 129 | } 130 | 131 | #[test(actix::test)] 132 | async fn should_stop_aggregate_process_when_unexpected_event_received( 133 | ) -> Result<(), Box> { 134 | let identity = Uuid::new_v4(); 135 | let addr = start_context(&identity).await; 136 | 137 | let base = ItemAppended(1); 138 | let event = RecordedEvent { 139 | event_number: 999, 140 | event_uuid: Uuid::new_v4(), 141 | stream_uuid: identity.to_string(), 142 | stream_version: Some(999), 143 | causation_id: None, 144 | correlation_id: None, 145 | event_type: base.event_type().to_string(), 146 | data: serde_json::to_value(base).unwrap(), 147 | metadata: None, 148 | created_at: chrono::offset::Utc::now(), 149 | }; 150 | 151 | assert_aggregate_version!(&addr, 0); 152 | assert_aggregate_state!( 153 | &addr, 154 | ExampleAggregate { 155 | items: vec![], 156 | last_index: 0 157 | } 158 | ); 159 | 160 | assert!(addr.send(ResolveAndApply(event.clone())).await?.is_err()); 161 | 162 | assert!(!addr.connected()); 163 | 164 | Ok(()) 165 | } 166 | -------------------------------------------------------------------------------- /crates/chekov/src/tests/aggregates/support/helpers.rs: -------------------------------------------------------------------------------- 1 | use actix::Addr; 2 | use uuid::Uuid; 3 | 4 | use crate::{ 5 | aggregate::{AggregateInstance, AggregateInstanceRegistry}, 6 | Application, 7 | }; 8 | 9 | use super::{ExampleAggregate, MyApplication}; 10 | 11 | #[allow(dead_code)] 12 | pub(crate) async fn start_context(identity: &Uuid) -> Addr> { 13 | start_application().await; 14 | start_aggregate(identity).await 15 | } 16 | 17 | pub(crate) async fn start_application() { 18 | MyApplication::with_default() 19 | .storage(event_store::storage::InMemoryStorage::initiate()) 20 | .launch() 21 | .await; 22 | } 23 | 24 | pub(crate) async fn start_aggregate(identity: &Uuid) -> Addr> { 25 | let correlation_id = Uuid::new_v4(); 26 | 27 | AggregateInstanceRegistry::::start_aggregate::( 28 | identity.to_string(), 29 | Some(correlation_id), 30 | ) 31 | .await 32 | .unwrap() 33 | } 34 | -------------------------------------------------------------------------------- /crates/chekov/src/tests/aggregates/support/mod.rs: -------------------------------------------------------------------------------- 1 | use crate as chekov; 2 | use crate::prelude::*; 3 | use event_store::prelude::InMemoryStorage; 4 | use serde::{Deserialize, Serialize}; 5 | use thiserror::Error; 6 | use uuid::Uuid; 7 | 8 | mod helpers; 9 | pub(crate) use helpers::*; 10 | 11 | #[derive(Default)] 12 | pub(crate) struct MyApplication {} 13 | 14 | impl Application for MyApplication { 15 | type Storage = InMemoryStorage; 16 | } 17 | 18 | #[derive(Debug, Error)] 19 | pub(crate) enum AggregateError { 20 | #[error("Invalid command received")] 21 | InvalidCommandError, 22 | } 23 | 24 | #[derive(Clone, Aggregate, Default, Debug, PartialEq)] 25 | #[aggregate(identity = "example")] 26 | pub(crate) struct ExampleAggregate { 27 | pub(crate) items: Vec, 28 | pub(crate) last_index: usize, 29 | } 30 | 31 | impl CommandExecutor for ExampleAggregate { 32 | fn execute(cmd: ValidCommand, _: &Self) -> ExecutionResult { 33 | ExecutionResult::Ok(vec![MyEvent { id: cmd.0 }]) 34 | } 35 | } 36 | 37 | impl CommandExecutor for ExampleAggregate { 38 | fn execute(_cmd: InvalidCommand, _: &Self) -> ExecutionResult { 39 | ExecutionResult::Err(CommandExecutorError::ExecutionError(Box::new( 40 | AggregateError::InvalidCommandError, 41 | ))) 42 | } 43 | } 44 | 45 | impl CommandExecutor for ExampleAggregate { 46 | fn execute(cmd: AppendItem, _: &Self) -> ExecutionResult { 47 | let mut events = vec![]; 48 | for i in 1..=cmd.0 { 49 | events.push(ItemAppended(i)); 50 | } 51 | ExecutionResult::Ok(events) 52 | } 53 | } 54 | 55 | #[crate::applier] 56 | impl EventApplier for ExampleAggregate { 57 | fn apply(&mut self, event: &ItemAppended) -> Result<(), ApplyError> { 58 | self.items.push(event.0); 59 | 60 | self.last_index = self.items.len() - 1; 61 | Ok(()) 62 | } 63 | } 64 | 65 | #[crate::applier] 66 | impl EventApplier for ExampleAggregate { 67 | fn apply(&mut self, _event: &InvalidEvent) -> Result<(), ApplyError> { 68 | Err(ApplyError::Any) 69 | } 70 | } 71 | 72 | #[crate::applier] 73 | impl EventApplier for ExampleAggregate { 74 | fn apply(&mut self, _event: &MyEvent) -> Result<(), ApplyError> { 75 | Ok(()) 76 | } 77 | } 78 | 79 | #[derive(Debug)] 80 | pub(crate) struct ValidCommand(pub(crate) Uuid); 81 | impl Command for ValidCommand { 82 | type Event = MyEvent; 83 | 84 | type Executor = ExampleAggregate; 85 | 86 | type ExecutorRegistry = AggregateInstanceRegistry; 87 | 88 | type CommandHandler = NoHandler; 89 | 90 | fn identifier(&self) -> String { 91 | self.0.to_string() 92 | } 93 | } 94 | 95 | #[derive(Debug)] 96 | pub(crate) struct InvalidCommand(pub(crate) Uuid); 97 | impl Command for InvalidCommand { 98 | type Event = InvalidEvent; 99 | 100 | type Executor = ExampleAggregate; 101 | 102 | type ExecutorRegistry = AggregateInstanceRegistry; 103 | 104 | type CommandHandler = NoHandler; 105 | 106 | fn identifier(&self) -> String { 107 | self.0.to_string() 108 | } 109 | } 110 | 111 | #[derive(Debug)] 112 | pub(crate) struct AppendItem(pub(crate) i64, pub(crate) Uuid); 113 | 114 | impl Command for AppendItem { 115 | type Event = ItemAppended; 116 | 117 | type Executor = ExampleAggregate; 118 | 119 | type ExecutorRegistry = AggregateInstanceRegistry; 120 | 121 | type CommandHandler = NoHandler; 122 | 123 | fn identifier(&self) -> String { 124 | self.1.to_string() 125 | } 126 | } 127 | 128 | #[derive(Clone, Debug, crate::Event, Deserialize, Serialize)] 129 | pub(crate) struct ItemAppended(pub(crate) i64); 130 | 131 | #[derive(Clone, PartialEq, Debug, crate::Event, Deserialize, Serialize)] 132 | #[event(event_type = "MyEvent")] 133 | pub(crate) struct MyEvent { 134 | pub(crate) id: Uuid, 135 | } 136 | 137 | #[derive(Clone, PartialEq, Debug, crate::Event, Deserialize, Serialize)] 138 | #[event(event_type = "InvalidEvent")] 139 | pub(crate) struct InvalidEvent { 140 | pub(crate) id: Uuid, 141 | } 142 | 143 | #[macro_export] 144 | macro_rules! assert_aggregate_version { 145 | ($instance: expr, $number: expr) => { 146 | let value = $instance.send($crate::message::AggregateVersion).await?; 147 | 148 | assert_eq!( 149 | value, $number, 150 | "Aggregate versions doesn't match => current: {}, expected: {}", 151 | value, $number 152 | ); 153 | }; 154 | } 155 | 156 | #[macro_export] 157 | macro_rules! assert_aggregate_state { 158 | ($instance: expr, $expected: expr) => { 159 | let value = $instance 160 | .send($crate::message::AggregateState(std::marker::PhantomData)) 161 | .await?; 162 | 163 | assert_eq!( 164 | value, $expected, 165 | "Aggregate state doesn't match => current: {:#?}, expected: {:#?}", 166 | value, $expected 167 | ); 168 | }; 169 | } 170 | -------------------------------------------------------------------------------- /crates/chekov/src/tests/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod aggregates; 2 | -------------------------------------------------------------------------------- /crates/event_store-backend-inmemory/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "event_store-backend-inmemory" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | uuid.workspace = true 10 | tracing.workspace = true 11 | serde_json.workspace = true 12 | tokio.workspace = true 13 | futures.workspace = true 14 | 15 | event_store-core = { version = "0.1.0", path = "../event_store-core" } 16 | chrono = { version = "0.4.19", features = ["serde"] } 17 | async-stream = "0.3" 18 | 19 | [dev-dependencies] 20 | test-log = { version = "0.2.8", default-features = false, features = ["trace"] } 21 | test-span = "0.1.1" 22 | tracing-subscriber = { version = "0.3", default-features = false, features = [ 23 | "env-filter", 24 | "fmt", 25 | ] } 26 | 27 | -------------------------------------------------------------------------------- /crates/event_store-backend-postgres/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "event_store-backend-postgres" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | uuid.workspace = true 10 | serde.workspace = true 11 | serde_json.workspace = true 12 | tracing.workspace = true 13 | sqlx.workspace = true 14 | futures.workspace = true 15 | 16 | event_store-core = { version = "0.1.0", path = "../event_store-core" } 17 | thiserror = "1.0" 18 | async-stream = "0.3" 19 | 20 | [dev-dependencies] 21 | pretty_assertions = "1.0.0" 22 | test-log = { version = "0.2.8", default-features = false, features = ["trace"] } 23 | test-span = "0.1.1" 24 | tracing-subscriber = { version = "0.3", default-features = false, features = [ 25 | "env-filter", 26 | "fmt", 27 | ] } 28 | 29 | 30 | -------------------------------------------------------------------------------- /crates/event_store-backend-postgres/src/error.rs: -------------------------------------------------------------------------------- 1 | use event_store_core::backend::error::BackendError; 2 | 3 | #[derive(thiserror::Error, Debug)] 4 | pub enum PostgresBackendError { 5 | #[error("Postgres SQL error: {0}")] 6 | SQLError(sqlx::Error), 7 | #[error("Postgres connection error: {0}")] 8 | PoolAcquisitionError(sqlx::Error), 9 | } 10 | 11 | impl From for PostgresBackendError { 12 | fn from(e: sqlx::Error) -> Self { 13 | Self::SQLError(e) 14 | } 15 | } 16 | 17 | impl BackendError for PostgresBackendError {} 18 | -------------------------------------------------------------------------------- /crates/event_store-core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "event_store-core" 3 | version = "0.1.0" 4 | edition = "2021" 5 | description = "Core struct and trait used to build event_store runtime/storage" 6 | license = "MIT OR Apache-2.0" 7 | 8 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 9 | 10 | [dependencies] 11 | actix = { workspace = true, optional = true } 12 | futures.workspace = true 13 | serde.workspace = true 14 | serde_json.workspace = true 15 | sqlx.workspace = true 16 | tokio.workspace = true 17 | uuid.workspace = true 18 | 19 | chrono = { version = "0.4.19", features = ["serde"] } 20 | thiserror = "1.0" 21 | 22 | [features] 23 | default = ["actix-rt"] 24 | verbose = [] 25 | 26 | actix-rt = ["actix"] 27 | 28 | 29 | -------------------------------------------------------------------------------- /crates/event_store-core/src/backend/error.rs: -------------------------------------------------------------------------------- 1 | pub trait BackendError: std::error::Error + 'static + Send + Sync {} 2 | -------------------------------------------------------------------------------- /crates/event_store-core/src/backend/mod.rs: -------------------------------------------------------------------------------- 1 | use futures::Future; 2 | use tokio::sync::mpsc; 3 | use uuid::Uuid; 4 | 5 | use crate::{ 6 | event::{RecordedEvent, UnsavedEvent}, 7 | event_bus::EventBusMessage, 8 | storage::StorageError, 9 | stream::Stream, 10 | }; 11 | 12 | pub mod error; 13 | 14 | pub trait Backend { 15 | fn backend_name() -> &'static str; 16 | 17 | #[doc(hidden)] 18 | fn direct_channel(&mut self, _notifier: mpsc::UnboundedSender) {} 19 | 20 | /// Create a new stream with an identifier 21 | /// 22 | /// # Errors 23 | /// The stream creation can fail for multiple reasons: 24 | /// 25 | /// - pure storage failure (unable to create the stream on the backend) 26 | /// - The stream already exists 27 | fn create_stream( 28 | &mut self, 29 | stream: Stream, 30 | correlation_id: Uuid, 31 | ) -> std::pin::Pin> + Send>>; 32 | 33 | /// Delete a stream from the `Backend` 34 | /// 35 | /// Do we need to provide a hard/soft deletion? 36 | /// 37 | /// # Errors 38 | /// The stream deletion can fail for multiple reasons: 39 | /// 40 | /// - pure storage failure (unable to delete the stream on the backend) 41 | /// - The stream doesn't exists 42 | fn delete_stream( 43 | &mut self, 44 | stream: &Stream, 45 | correlation_id: Uuid, 46 | ) -> std::pin::Pin> + Send>>; 47 | 48 | fn append_to_stream( 49 | &mut self, 50 | stream_uuid: &str, 51 | events: &[UnsavedEvent], 52 | correlation_id: Uuid, 53 | ) -> std::pin::Pin, StorageError>> + Send>>; 54 | 55 | // async fn read_stream( 56 | fn read_stream( 57 | &self, 58 | stream_uuid: String, 59 | version: usize, 60 | limit: usize, 61 | correlation_id: Uuid, 62 | ) -> std::pin::Pin, StorageError>> + Send>>; 63 | 64 | fn read_stream_info( 65 | &mut self, 66 | stream_uuid: String, 67 | correlation_id: Uuid, 68 | ) -> std::pin::Pin> + Send>>; 69 | 70 | fn stream_forward( 71 | &self, 72 | stream_uuid: String, 73 | batch_size: usize, 74 | correlation_id: Uuid, 75 | ) -> std::pin::Pin< 76 | Box, StorageError>> + Send>, 77 | >; 78 | } 79 | -------------------------------------------------------------------------------- /crates/event_store-core/src/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | use crate::{event::UnsavedEventError, storage::StorageError}; 4 | 5 | #[derive(Error, Debug)] 6 | pub enum EventStoreError { 7 | #[error("No storage is defined")] 8 | NoStorage, 9 | #[error("The streamId is invalid")] 10 | InvalidStreamId, 11 | #[error(transparent)] 12 | Storage(StorageError), 13 | #[error(transparent)] 14 | EventProcessing(UnsavedEventError), 15 | #[error("Internal event store error: {0}")] 16 | InternalEventStoreError(#[source] BoxDynError), 17 | } 18 | 19 | impl From for EventStoreError { 20 | fn from(error: StorageError) -> Self { 21 | Self::Storage(error) 22 | } 23 | } 24 | 25 | impl std::convert::From for EventStoreError { 26 | fn from(e: UnsavedEventError) -> Self { 27 | Self::EventProcessing(e) 28 | } 29 | } 30 | 31 | pub type BoxDynError = Box; 32 | 33 | #[cfg(feature = "actix-rt")] 34 | impl From for EventStoreError { 35 | fn from(error: actix::MailboxError) -> Self { 36 | Self::InternalEventStoreError(Box::new(error)) 37 | } 38 | } 39 | #[cfg(test)] 40 | mod test { 41 | use super::*; 42 | 43 | #[test] 44 | fn testing_that_a_mailboxerror_can_be_converted() { 45 | let err = actix::MailboxError::Closed; 46 | 47 | let _c: EventStoreError = err.into(); 48 | } 49 | 50 | #[test] 51 | fn testing_that_a_parse_event_error_can_be_converted() { 52 | let error = serde_json::from_str::<'_, String>("{").unwrap_err(); 53 | let err = UnsavedEventError::SerializeError(error); 54 | 55 | let _: EventStoreError = err.into(); 56 | } 57 | 58 | #[test] 59 | fn an_event_store_error_can_be_dispayed() { 60 | let err = EventStoreError::InvalidStreamId; 61 | 62 | assert_eq!("The streamId is invalid", err.to_string()); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /crates/event_store-core/src/event/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | /// Errors related to a recorded event 4 | #[derive(Error, Debug)] 5 | pub enum RecordedEventError { 6 | #[error("Unable to deserialize the recorded event")] 7 | DeserializeError(serde_json::Error), 8 | } 9 | 10 | /// Errors related to a unsaved event 11 | #[derive(Error, Debug)] 12 | pub enum UnsavedEventError { 13 | #[error("Unable to serialize the event")] 14 | SerializeError(serde_json::Error), 15 | } 16 | 17 | impl From for UnsavedEventError { 18 | fn from(e: serde_json::Error) -> Self { 19 | Self::SerializeError(e) 20 | } 21 | } 22 | impl From for RecordedEventError { 23 | fn from(e: serde_json::Error) -> Self { 24 | Self::DeserializeError(e) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /crates/event_store-core/src/event/mod.rs: -------------------------------------------------------------------------------- 1 | use actix::Message; 2 | use chrono::DateTime; 3 | use chrono::Utc; 4 | use serde::Serialize; 5 | use serde_json::json; 6 | use uuid::Uuid; 7 | 8 | pub mod error; 9 | 10 | pub use error::*; 11 | 12 | #[cfg(test)] 13 | mod test; 14 | 15 | /// Represent event that can be handled by an `EventStore` 16 | pub trait Event: Serialize + Send + std::convert::TryFrom { 17 | /// Returns a `'static str` which defines the event type 18 | /// 19 | /// This `str` must be as precise as possible. 20 | fn event_type(&self) -> &'static str; 21 | 22 | /// Returns every possible string representations of the event. 23 | /// 24 | /// Useful to define particular variant types for an enum 25 | fn all_event_types() -> Vec<&'static str>; 26 | } 27 | 28 | /// A `RecordedEvent` represents an `Event` which have been append to a `Stream` 29 | #[derive(sqlx::FromRow, Debug, Clone, Message, Serialize)] 30 | #[rtype(result = "()")] 31 | pub struct RecordedEvent { 32 | /// an incrementing and gapless integer used to order the event in a stream. 33 | #[sqlx(try_from = "i64")] 34 | pub event_number: u64, 35 | /// Unique identifier for this event 36 | pub event_uuid: Uuid, 37 | /// The stream identifier for thie event 38 | pub stream_uuid: String, 39 | /// The stream version when this event was appended 40 | pub stream_version: Option, 41 | /// a `causation_id` defines who caused this event 42 | pub causation_id: Option, 43 | /// a `correlation_id` correlates multiple events 44 | pub correlation_id: Option, 45 | /// Human readable event type 46 | pub event_type: String, 47 | /// Payload of this event 48 | pub data: serde_json::Value, 49 | /// Metadata defined for this event 50 | pub metadata: Option, 51 | /// Event time creation 52 | pub created_at: DateTime, 53 | } 54 | 55 | impl RecordedEvent { 56 | /// # Errors 57 | pub fn try_deserialize< 58 | 'de, 59 | T: serde::Deserialize<'de> + Event + serde::de::Deserialize<'de>, 60 | >( 61 | &'de self, 62 | ) -> Result { 63 | Ok(T::deserialize(&self.data)?) 64 | } 65 | } 66 | 67 | /// An `UnsavedEvent` is created from a type that implement `Event` 68 | /// 69 | /// This kind of event represents an unsaved event, meaning that it has less informations 70 | /// than a `RecordedEvent`. It's a generic form to simplify the event processing but also a way to 71 | /// define `metadata`, `causation_id` and `correlation_id`. 72 | #[derive(Debug, Clone, PartialEq)] 73 | pub struct UnsavedEvent { 74 | /// a `causation_id` defines who caused this event 75 | pub causation_id: Option, 76 | /// a `correlation_id` correlates multiple events 77 | pub correlation_id: Option, 78 | /// Human readable event type 79 | pub event_type: String, 80 | /// Payload of this event 81 | pub data: serde_json::Value, 82 | /// Metadata defined for this event 83 | pub metadata: serde_json::Value, 84 | pub event_uuid: Uuid, 85 | pub stream_uuid: String, 86 | pub stream_version: u64, 87 | pub created_at: DateTime, 88 | } 89 | 90 | impl UnsavedEvent { 91 | /// Try to create an `UnsavedEvent` from a struct that implement `Event`. 92 | /// 93 | /// In case of a success an `UnsavedEvent` is returned with no context or metadata. 94 | /// 95 | /// # Errors 96 | /// If `serde` isn't able to serialize the `Event` an `UnsavedEventError::SerializeError` is 97 | /// returned 98 | pub fn try_from(event: &E) -> Result { 99 | Ok(Self { 100 | causation_id: None, 101 | correlation_id: None, 102 | event_type: event.event_type().to_owned(), 103 | data: serde_json::to_value(event)?, 104 | metadata: json!({}), 105 | event_uuid: Uuid::new_v4(), 106 | stream_uuid: String::new(), 107 | stream_version: 0, 108 | created_at: Utc::now(), 109 | }) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /crates/event_store-core/src/event/test.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use serde::Serialize; 3 | 4 | #[derive(Serialize, serde::Deserialize)] 5 | pub struct MyStructEvent {} 6 | 7 | #[derive(Serialize, serde::Deserialize)] 8 | pub enum MyEnumEvent { 9 | Created { id: i32 }, 10 | Updated(String), 11 | Deleted, 12 | } 13 | 14 | impl Event for MyEnumEvent { 15 | fn event_type(&self) -> &'static str { 16 | match *self { 17 | Self::Deleted => "MyEnumEvent::Deleted", 18 | Self::Created { .. } => "MyEnumEvent::Created", 19 | Self::Updated(_) => "MyEnumEvent::Updated", 20 | } 21 | } 22 | 23 | fn all_event_types() -> Vec<&'static str> { 24 | vec![ 25 | "MyEnumEvent::Deleted", 26 | "MyEnumEvent::Collect", 27 | "MyEnumEvent::Updated", 28 | ] 29 | } 30 | } 31 | 32 | impl Event for MyStructEvent { 33 | fn event_type(&self) -> &'static str { 34 | "MyStructEvent" 35 | } 36 | 37 | fn all_event_types() -> Vec<&'static str> { 38 | vec!["MyStructEvent"] 39 | } 40 | } 41 | 42 | impl std::convert::TryFrom for MyEnumEvent { 43 | type Error = (); 44 | fn try_from(e: RecordedEvent) -> Result { 45 | serde_json::from_value(e.data).map_err(|_| ()) 46 | } 47 | } 48 | impl std::convert::TryFrom for MyStructEvent { 49 | type Error = (); 50 | fn try_from(e: RecordedEvent) -> Result { 51 | serde_json::from_value(e.data).map_err(|_| ()) 52 | } 53 | } 54 | 55 | mod unsaved { 56 | use super::*; 57 | 58 | #[test] 59 | fn must_have_a_valide_event_type() { 60 | let source_events = vec![ 61 | MyEnumEvent::Created { id: 1 }, 62 | MyEnumEvent::Updated("Updated".into()), 63 | MyEnumEvent::Deleted, 64 | ]; 65 | 66 | let mut produces_events: Vec = source_events 67 | .iter() 68 | .map(UnsavedEvent::try_from) 69 | .filter(Result::is_ok) 70 | .map(Result::unwrap) 71 | .collect(); 72 | 73 | let next = MyStructEvent {}; 74 | 75 | produces_events.push(UnsavedEvent::try_from(&next).unwrap()); 76 | 77 | let expected = vec![ 78 | "MyEnumEvent::Created", 79 | "MyEnumEvent::Updated", 80 | "MyEnumEvent::Deleted", 81 | "MyStructEvent", 82 | ]; 83 | 84 | assert_eq!(expected.len(), produces_events.len()); 85 | expected 86 | .into_iter() 87 | .zip(produces_events) 88 | .for_each(|(ex, real)| assert_eq!(ex, real.event_type)); 89 | } 90 | 91 | #[test] 92 | fn test_that_serde_error_are_handled() { 93 | use serde::ser::Error; 94 | let err = UnsavedEventError::from(serde_json::Error::custom("test")); 95 | 96 | let _: UnsavedEventError = err; 97 | } 98 | 99 | #[derive(serde::Serialize, serde::Deserialize)] 100 | struct MyEvent(pub String); 101 | 102 | impl Event for MyEvent { 103 | fn event_type(&self) -> &'static str { 104 | "MyEvent" 105 | } 106 | 107 | fn all_event_types() -> Vec<&'static str> { 108 | vec!["MyEvent"] 109 | } 110 | } 111 | 112 | impl std::convert::TryFrom for MyEvent { 113 | type Error = (); 114 | fn try_from(e: RecordedEvent) -> Result { 115 | serde_json::from_value(e.data).map_err(|_| ()) 116 | } 117 | } 118 | 119 | #[test] 120 | fn test_that_ids_can_be_setted() { 121 | let event = MyEvent("Hello".into()); 122 | let _unsaved = match UnsavedEvent::try_from(&event) { 123 | Ok(unsaved) => { 124 | assert!(unsaved.causation_id.is_none()); 125 | assert!(unsaved.correlation_id.is_none()); 126 | assert_eq!(unsaved.event_type, "MyEvent"); 127 | assert_eq!(unsaved.event_type, event.event_type()); 128 | assert_eq!(unsaved.data, serde_json::Value::String("Hello".into())); 129 | assert_eq!(unsaved.metadata, json!({})); 130 | unsaved 131 | } 132 | Err(_) => panic!("Couldnt convert into UnsavedEvent"), 133 | }; 134 | 135 | let _expected = UnsavedEvent { 136 | causation_id: None, 137 | correlation_id: None, 138 | event_type: "MyEvent".into(), 139 | data: "\"Hello\"".into(), 140 | metadata: json!({}), 141 | event_uuid: Uuid::new_v4(), 142 | stream_uuid: String::new(), 143 | stream_version: 0, 144 | created_at: chrono::offset::Utc::now(), 145 | }; 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /crates/event_store-core/src/event_bus/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | // Convenience type alias for usage within event_store. 4 | type BoxDynError = Box; 5 | 6 | #[derive(Error, Debug)] 7 | pub enum EventBusError { 8 | #[error("Notification error: {0}")] 9 | EventNotificationError(EventNotificationError), 10 | #[error("Internal storage error: {0}")] 11 | InternalEventBusError(#[source] BoxDynError), 12 | } 13 | 14 | #[derive(Error, Debug)] 15 | pub enum EventNotificationError { 16 | #[error("Unable to parse {field}")] 17 | ParsingError { field: &'static str }, 18 | #[error("Invalid stream_uuid")] 19 | InvalidStreamUUID, 20 | } 21 | -------------------------------------------------------------------------------- /crates/event_store-core/src/event_bus/mod.rs: -------------------------------------------------------------------------------- 1 | use std::pin::Pin; 2 | 3 | use actix::Message; 4 | use futures::{Future, Stream}; 5 | 6 | use crate::event::RecordedEvent; 7 | 8 | use self::error::{EventBusError, EventNotificationError}; 9 | 10 | pub mod error; 11 | 12 | pub trait EventBus: std::fmt::Debug + Default + Send + std::marker::Unpin + 'static { 13 | fn bus_name() -> &'static str; 14 | 15 | // fn prepare + 'static>(&mut self, resolver: F) -> BoxFuture<'static, ()>; 16 | 17 | fn create_stream(&mut self) -> BoxedStream; 18 | } 19 | 20 | pub type BoxedStream = Pin< 21 | Box< 22 | dyn Future< 23 | Output = Pin> + Send>>, 24 | > + Send, 25 | >, 26 | >; 27 | 28 | #[derive(Debug, Message)] 29 | #[rtype(result = "()")] 30 | pub enum EventBusMessage { 31 | Notification(EventNotification), 32 | Events(String, Vec), 33 | Unkown, 34 | } 35 | 36 | /// Notification produced by the `EventBus` which contains events/streams related informations 37 | #[derive(Clone, Debug, Message)] 38 | #[rtype(result = "()")] 39 | pub struct EventNotification { 40 | pub stream_id: i32, 41 | pub stream_uuid: String, 42 | pub first_stream_version: u32, 43 | pub last_stream_version: u32, 44 | } 45 | 46 | impl<'a> TryFrom<&'a str> for EventNotification { 47 | type Error = EventNotificationError; 48 | 49 | fn try_from(value: &'a str) -> Result { 50 | let mut through = value.splitn(4, ','); 51 | 52 | let stream_uuid = if let Ok(stream_uuid) = through 53 | .next() 54 | .ok_or(EventNotificationError::ParsingError { 55 | field: "stream_uuid", 56 | })? 57 | .parse::() 58 | { 59 | if stream_uuid.is_empty() { 60 | return Err(EventNotificationError::InvalidStreamUUID); 61 | } 62 | stream_uuid 63 | } else { 64 | return Err(EventNotificationError::ParsingError { 65 | field: "stream_uuid", 66 | }); 67 | }; 68 | 69 | let stream_id = through 70 | .next() 71 | .ok_or(EventNotificationError::ParsingError { field: "stream_id" })? 72 | .parse::() 73 | .or(Err(EventNotificationError::ParsingError { 74 | field: "stream_id", 75 | }))?; 76 | 77 | let first_stream_version = through 78 | .next() 79 | .ok_or(EventNotificationError::ParsingError { 80 | field: "first_stream_version", 81 | })? 82 | .parse::() 83 | .or(Err(EventNotificationError::ParsingError { 84 | field: "first_stream_version", 85 | }))?; 86 | 87 | let last_stream_version = through 88 | .next() 89 | .ok_or(EventNotificationError::ParsingError { 90 | field: "last_stream_version", 91 | })? 92 | .parse::() 93 | .or(Err(EventNotificationError::ParsingError { 94 | field: "last_stream_version", 95 | }))?; 96 | 97 | Ok(Self { 98 | stream_uuid, 99 | stream_id, 100 | first_stream_version, 101 | last_stream_version, 102 | }) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /crates/event_store-core/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod backend; 2 | pub mod error; 3 | pub mod event; 4 | pub mod event_bus; 5 | pub mod storage; 6 | pub mod stream; 7 | pub mod versions; 8 | -------------------------------------------------------------------------------- /crates/event_store-core/src/storage/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | use crate::backend::error::BackendError; 4 | 5 | #[derive(Error, Debug)] 6 | pub enum StorageError { 7 | #[error("The stream doesn't exists")] 8 | StreamDoesntExists, 9 | #[error("The stream already exists")] 10 | StreamAlreadyExists, 11 | #[error("BackendError: {0}")] 12 | InternalBackendError(Box), 13 | } 14 | 15 | impl From for StorageError 16 | where 17 | T: BackendError, 18 | { 19 | fn from(e: T) -> Self { 20 | Self::InternalBackendError(Box::new(e)) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /crates/event_store-core/src/storage/mod.rs: -------------------------------------------------------------------------------- 1 | use tokio::sync::mpsc; 2 | 3 | use crate::{ 4 | backend::Backend, 5 | event_bus::{BoxedStream, EventBus, EventBusMessage}, 6 | }; 7 | 8 | pub use self::error::StorageError; 9 | pub mod error; 10 | 11 | /// A `Storage` is responsible for storing and managing `Stream` and `Event`for a `Backend` 12 | pub trait Storage: std::fmt::Debug + Default + Send + std::marker::Unpin + 'static { 13 | type Backend: Backend; 14 | type EventBus: EventBus; 15 | 16 | fn storage_name() -> &'static str; 17 | 18 | #[doc(hidden)] 19 | fn direct_channel(&mut self, _notifier: mpsc::UnboundedSender) {} 20 | 21 | fn create_stream(&mut self) -> BoxedStream; 22 | fn backend(&mut self) -> &mut Self::Backend; 23 | } 24 | -------------------------------------------------------------------------------- /crates/event_store-core/src/stream/error.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, PartialEq)] 2 | pub enum StreamError { 3 | MalformedStreamUUID, 4 | } 5 | -------------------------------------------------------------------------------- /crates/event_store-core/src/stream/mod.rs: -------------------------------------------------------------------------------- 1 | use chrono::DateTime; 2 | 3 | pub mod error; 4 | 5 | use error::StreamError; 6 | 7 | /// A `Stream` represents an `Event` stream 8 | #[derive(Clone, Debug, PartialEq, sqlx::FromRow)] 9 | pub struct Stream { 10 | #[sqlx(try_from = "i64")] 11 | pub stream_id: u64, 12 | /// The stream identifier which is unique 13 | pub stream_uuid: String, 14 | /// The current stream version number 15 | #[sqlx(try_from = "i64")] 16 | pub stream_version: u64, 17 | /// The creation date of the stream 18 | pub created_at: DateTime, 19 | /// The deletion date of the stream 20 | pub deleted_at: Option>, 21 | } 22 | 23 | impl Stream { 24 | #[must_use] 25 | pub fn stream_uuid(&self) -> &str { 26 | self.stream_uuid.as_ref() 27 | } 28 | 29 | pub const fn is_persisted(&self) -> bool { 30 | self.stream_id != 0 31 | } 32 | 33 | pub fn validates_stream_id(stream_id: &str) -> bool { 34 | !stream_id.contains(' ') 35 | } 36 | } 37 | 38 | impl std::str::FromStr for Stream { 39 | type Err = StreamError; 40 | 41 | fn from_str(s: &str) -> Result { 42 | if !Self::validates_stream_id(s) { 43 | return Err(StreamError::MalformedStreamUUID); 44 | } 45 | 46 | Ok(Self { 47 | stream_id: 0, 48 | stream_uuid: s.into(), 49 | stream_version: 0, 50 | created_at: chrono::Utc::now(), 51 | deleted_at: None, 52 | }) 53 | } 54 | } 55 | 56 | #[cfg(test)] 57 | mod test { 58 | use super::*; 59 | 60 | use std::str::FromStr; 61 | 62 | #[test] 63 | fn test_stream_from_str() { 64 | assert_eq!( 65 | Err(StreamError::MalformedStreamUUID), 66 | Stream::from_str("unvalid stream name") 67 | ); 68 | } 69 | 70 | #[test] 71 | fn test_stream_is_not_persisted_by_default() { 72 | assert!(!Stream::from_str("unvalid").unwrap().is_persisted()); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /crates/event_store-core/src/versions.rs: -------------------------------------------------------------------------------- 1 | use crate::stream::Stream; 2 | 3 | #[derive(Debug)] 4 | pub enum ReadVersion { 5 | Origin, 6 | Version(u64), 7 | } 8 | 9 | /// The `ExpectedVersion` used to define optimistic concurrency 10 | #[derive(Debug, PartialEq)] 11 | pub enum ExpectedVersion { 12 | /// Define that we expect a stream in any version 13 | AnyVersion, 14 | /// Define that we expect a non existing stream 15 | NoStream, 16 | /// Define that we expect an existing stream 17 | StreamExists, 18 | /// Define that we expect a stream in a particular version 19 | Version(u64), 20 | } 21 | 22 | impl ExpectedVersion { 23 | #[must_use] 24 | pub const fn verify(stream: &Stream, expected: &Self) -> ExpectedVersionResult { 25 | match expected { 26 | _ if stream.is_persisted() && stream.deleted_at.is_some() => { 27 | ExpectedVersionResult::StreamDeleted 28 | } 29 | Self::NoStream | Self::Version(0) | Self::AnyVersion if !stream.is_persisted() => { 30 | ExpectedVersionResult::NeedCreation 31 | } 32 | 33 | Self::StreamExists if !stream.is_persisted() => { 34 | ExpectedVersionResult::StreamDoesntExists 35 | } 36 | 37 | Self::AnyVersion | Self::StreamExists if stream.is_persisted() => { 38 | ExpectedVersionResult::Ok 39 | } 40 | 41 | Self::Version(version) 42 | if stream.is_persisted() && stream.stream_version == *version => 43 | { 44 | ExpectedVersionResult::Ok 45 | } 46 | 47 | Self::NoStream if stream.is_persisted() && stream.stream_version != 0 => { 48 | ExpectedVersionResult::StreamAlreadyExists 49 | } 50 | 51 | Self::NoStream if stream.is_persisted() && stream.stream_version == 0 => { 52 | ExpectedVersionResult::Ok 53 | } 54 | _ => ExpectedVersionResult::WrongExpectedVersion, 55 | } 56 | } 57 | } 58 | 59 | #[derive(Debug, PartialEq)] 60 | pub enum ExpectedVersionResult { 61 | NeedCreation, 62 | Ok, 63 | StreamAlreadyExists, 64 | StreamDeleted, 65 | StreamDoesntExists, 66 | WrongExpectedVersion, 67 | } 68 | 69 | #[cfg(test)] 70 | mod test { 71 | use super::*; 72 | use chrono::Utc; 73 | use std::str::FromStr; 74 | 75 | #[test] 76 | fn test_that_an_unexisting_stream_with_any_is_created() { 77 | let stream = Stream::from_str("stream_1").unwrap(); 78 | 79 | assert_eq!( 80 | ExpectedVersion::verify(&stream, &ExpectedVersion::AnyVersion), 81 | ExpectedVersionResult::NeedCreation 82 | ); 83 | } 84 | 85 | #[test] 86 | fn test_validation_result_need_creation() { 87 | let mut stream = Stream::from_str("stream_1").unwrap(); 88 | stream.stream_id = 1; 89 | stream.deleted_at = Some(Utc::now()); 90 | 91 | assert_eq!( 92 | ExpectedVersion::verify(&stream, &ExpectedVersion::AnyVersion), 93 | ExpectedVersionResult::StreamDeleted 94 | ); 95 | 96 | stream.stream_id = 0; 97 | stream.deleted_at = None; 98 | 99 | assert_eq!( 100 | ExpectedVersion::verify(&stream, &ExpectedVersion::NoStream), 101 | ExpectedVersionResult::NeedCreation 102 | ); 103 | 104 | assert_eq!( 105 | ExpectedVersion::verify(&stream, &ExpectedVersion::Version(0)), 106 | ExpectedVersionResult::NeedCreation 107 | ); 108 | 109 | assert_eq!( 110 | ExpectedVersion::verify(&stream, &ExpectedVersion::AnyVersion), 111 | ExpectedVersionResult::NeedCreation 112 | ); 113 | 114 | assert_eq!( 115 | ExpectedVersion::verify(&stream, &ExpectedVersion::StreamExists), 116 | ExpectedVersionResult::StreamDoesntExists 117 | ); 118 | 119 | stream.stream_id = 1; 120 | assert_eq!( 121 | ExpectedVersion::verify(&stream, &ExpectedVersion::AnyVersion), 122 | ExpectedVersionResult::Ok 123 | ); 124 | 125 | assert_eq!( 126 | ExpectedVersion::verify(&stream, &ExpectedVersion::StreamExists), 127 | ExpectedVersionResult::Ok 128 | ); 129 | 130 | stream.stream_version = 1; 131 | 132 | assert_eq!( 133 | ExpectedVersion::verify(&stream, &ExpectedVersion::Version(1)), 134 | ExpectedVersionResult::Ok 135 | ); 136 | 137 | assert_eq!( 138 | ExpectedVersion::verify(&stream, &ExpectedVersion::NoStream), 139 | ExpectedVersionResult::StreamAlreadyExists 140 | ); 141 | 142 | stream.stream_version = 0; 143 | 144 | assert_eq!( 145 | ExpectedVersion::verify(&stream, &ExpectedVersion::NoStream), 146 | ExpectedVersionResult::Ok 147 | ); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /crates/event_store-eventbus-inmemory/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "event_store-eventbus-inmemory" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | uuid.workspace = true 10 | serde.workspace = true 11 | actix.workspace = true 12 | futures.workspace = true 13 | tokio.workspace = true 14 | 15 | async-stream = "0.3" 16 | 17 | event_store-core = { version = "0.1.0", path = "../event_store-core" } 18 | 19 | [dev-dependencies] 20 | test-log = { version = "0.2.8", default-features = false, features = ["trace"] } 21 | test-span = "0.1.1" 22 | tracing-subscriber = { version = "0.3", default-features = false, features = [ 23 | "env-filter", 24 | "fmt", 25 | ] } 26 | 27 | -------------------------------------------------------------------------------- /crates/event_store-eventbus-inmemory/src/lib.rs: -------------------------------------------------------------------------------- 1 | use actix::prelude::*; 2 | use async_stream::try_stream; 3 | use event_store_core::event_bus::{error::EventBusError, BoxedStream, EventBus, EventBusMessage}; 4 | use futures::FutureExt; 5 | use std::pin::Pin; 6 | use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender}; 7 | 8 | #[derive(Debug)] 9 | pub struct InMemoryEventBus { 10 | receiver: Option>, 11 | pub sender: UnboundedSender, 12 | } 13 | 14 | impl Default for InMemoryEventBus { 15 | fn default() -> Self { 16 | let (sender, receiver) = mpsc::unbounded_channel::(); 17 | 18 | Self { 19 | receiver: Some(receiver), 20 | sender, 21 | } 22 | } 23 | } 24 | 25 | impl InMemoryEventBus { 26 | pub async fn initiate() -> Result { 27 | Ok(Self::default()) 28 | } 29 | 30 | async fn start_listening( 31 | mut receiver: UnboundedReceiver, 32 | ) -> Pin> + Send>> { 33 | Box::pin(try_stream! { 34 | while let Some(event) = receiver.recv().await { 35 | yield event; 36 | } 37 | }) 38 | } 39 | } 40 | 41 | impl EventBus for InMemoryEventBus { 42 | fn bus_name() -> &'static str { 43 | "InMemoryEventBus" 44 | } 45 | 46 | // fn prepare(&mut self, storage: Addr>) -> BoxFuture<'static, ()> { 47 | // let storage = storage.clone(); 48 | // let sender = self.sender.clone(); 49 | // async move { 50 | // let _ = storage.send(OpenNotificationChannel { sender }).await; 51 | // } 52 | // .boxed() 53 | // } 54 | 55 | fn create_stream(&mut self) -> BoxedStream { 56 | let receiver = self.receiver.take().unwrap(); 57 | 58 | Self::start_listening(receiver).boxed() 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /crates/event_store-eventbus-postgres/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "event_store-eventbus-postgres" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | uuid.workspace = true 8 | serde.workspace = true 9 | sqlx = { workspace = true, features = ["postgres"] } 10 | futures.workspace = true 11 | 12 | event_store-core = { version = "0.1.0", path = "../event_store-core" } 13 | -------------------------------------------------------------------------------- /crates/event_store-eventbus-postgres/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::pin::Pin; 2 | 3 | use event_store_core::event_bus::{ 4 | error::EventBusError, BoxedStream, EventBus, EventBusMessage, EventNotification, 5 | }; 6 | use futures::{FutureExt, Stream, StreamExt}; 7 | use sqlx::postgres::PgListener; 8 | 9 | #[derive(Debug)] 10 | pub struct PostgresEventBus { 11 | listener: Option, 12 | } 13 | 14 | impl Default for PostgresEventBus { 15 | fn default() -> Self { 16 | unimplemented!() 17 | } 18 | } 19 | 20 | impl PostgresEventBus { 21 | pub async fn initiate(url: String) -> Result { 22 | let listener = sqlx::postgres::PgListener::connect(&url).await.unwrap(); 23 | 24 | Ok(Self { 25 | listener: Some(listener), 26 | }) 27 | } 28 | 29 | async fn start_listening( 30 | mut listener: PgListener, 31 | ) -> Pin> + Send>> { 32 | listener.listen("events").await.unwrap(); 33 | 34 | listener 35 | .into_stream() 36 | .map(|res| match res { 37 | Ok(notification) => { 38 | if let Ok(event) = EventNotification::try_from(notification.payload()) { 39 | return Ok(EventBusMessage::Notification(event)); 40 | } 41 | 42 | Ok(EventBusMessage::Unkown) 43 | } 44 | Err(e) => Err(EventBusError::InternalEventBusError(Box::new(e))), 45 | }) 46 | .boxed() 47 | } 48 | } 49 | 50 | impl EventBus for PostgresEventBus { 51 | fn bus_name() -> &'static str { 52 | "PostgresEventBus" 53 | } 54 | 55 | // fn prepare(&mut self, _: Addr>) -> BoxFuture<'static, ()> { 56 | // future::ready(()).boxed() 57 | // } 58 | 59 | fn create_stream(&mut self) -> BoxedStream { 60 | let listener = self.listener.take().unwrap(); 61 | Self::start_listening(listener).boxed() 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /crates/event_store-storage-inmemory/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "event_store-storage-inmemory" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | event_store-core = { version = "0.1.0", path = "../event_store-core" } 10 | event_store-backend-inmemory = { version = "0.1.0", path = "../event_store-backend-inmemory" } 11 | event_store-eventbus-inmemory = { version = "0.1.0", path = "../event_store-eventbus-inmemory" } 12 | 13 | tokio = { version = "1.12.0", features = ["full"] } 14 | -------------------------------------------------------------------------------- /crates/event_store-storage-inmemory/src/lib.rs: -------------------------------------------------------------------------------- 1 | use event_store_backend_inmemory::InMemoryBackend; 2 | use event_store_core::{ 3 | event_bus::{BoxedStream, EventBus, EventBusMessage}, 4 | storage::Storage, 5 | }; 6 | use event_store_eventbus_inmemory::InMemoryEventBus; 7 | use tokio::sync::mpsc::{self}; 8 | 9 | /// InMemory storage used for tests mostly 10 | #[derive(Default, Debug)] 11 | pub struct InMemoryStorage { 12 | backend: InMemoryBackend, 13 | event_bus: InMemoryEventBus, 14 | } 15 | 16 | impl InMemoryStorage { 17 | pub async fn initiate() -> Result { 18 | Ok(Self::default()) 19 | } 20 | } 21 | 22 | impl Storage for InMemoryStorage { 23 | type Backend = InMemoryBackend; 24 | type EventBus = InMemoryEventBus; 25 | 26 | fn storage_name() -> &'static str { 27 | "InMemory" 28 | } 29 | 30 | fn direct_channel(&mut self, notifier: mpsc::UnboundedSender) { 31 | self.backend.notifier = Some(notifier); 32 | } 33 | 34 | fn create_stream(&mut self) -> BoxedStream { 35 | self.backend.notifier = Some(self.event_bus.sender.clone()); 36 | self.event_bus.create_stream() 37 | } 38 | 39 | fn backend(&mut self) -> &mut Self::Backend { 40 | &mut self.backend 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /crates/event_store-storage-postgres/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "event_store-storage-postgres" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | event_store-core = { version = "0.1.0", path = "../event_store-core" } 10 | event_store-backend-postgres = { version = "0.1.0", path = "../event_store-backend-postgres" } 11 | event_store-eventbus-postgres = { version = "0.1.0", path = "../event_store-eventbus-postgres" } 12 | 13 | sqlx.workspace = true 14 | tracing.workspace = true 15 | -------------------------------------------------------------------------------- /crates/event_store-storage-postgres/src/lib.rs: -------------------------------------------------------------------------------- 1 | use event_store_backend_postgres::PostgresBackend; 2 | use event_store_core::{ 3 | event_bus::{BoxedStream, EventBus}, 4 | storage::Storage, 5 | }; 6 | use event_store_eventbus_postgres::PostgresEventBus; 7 | 8 | #[derive(Debug, Default)] 9 | pub struct PostgresStorage { 10 | backend: PostgresBackend, 11 | event_bus: PostgresEventBus, 12 | } 13 | 14 | impl PostgresStorage { 15 | /// # Errors 16 | /// 17 | /// In case of Postgres connection error 18 | #[tracing::instrument(name = "PostgresBackend", skip(url))] 19 | pub async fn with_url(url: &str) -> Result { 20 | Ok(Self { 21 | backend: PostgresBackend::with_url(url).await?, 22 | // TODO Add DatabaseError convertor 23 | event_bus: PostgresEventBus::initiate(url.into()).await.unwrap(), 24 | }) 25 | } 26 | } 27 | 28 | impl Storage for PostgresStorage { 29 | type Backend = PostgresBackend; 30 | type EventBus = PostgresEventBus; 31 | 32 | fn storage_name() -> &'static str { 33 | "Postgres" 34 | } 35 | 36 | fn backend(&mut self) -> &mut Self::Backend { 37 | &mut self.backend 38 | } 39 | 40 | fn create_stream(&mut self) -> BoxedStream { 41 | self.event_bus.create_stream() 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /crates/event_store/.env: -------------------------------------------------------------------------------- 1 | DATABASE_URL=postgresql://postgres:postgres@localhost:5432/event_store_bank 2 | -------------------------------------------------------------------------------- /crates/event_store/.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | -------------------------------------------------------------------------------- /crates/event_store/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "event_store" 3 | version = "0.1.1" 4 | authors = ["Freyskeyd "] 5 | edition = "2018" 6 | description = "Crate to deal with every aspect of an eventstore" 7 | license = "MIT" 8 | 9 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 10 | 11 | [dependencies] 12 | uuid.workspace = true 13 | serde.workspace = true 14 | serde_json.workspace = true 15 | actix.workspace = true 16 | tracing.workspace = true 17 | tokio.workspace = true 18 | futures.workspace = true 19 | 20 | event_store-core = { version = "0.1.0", path = "../event_store-core" } 21 | 22 | event_store-storage-inmemory = { version = "0.1.0", path = "../event_store-storage-inmemory", optional = true } 23 | event_store-backend-inmemory = { version = "0.1.0", path = "../event_store-backend-inmemory", optional = true } 24 | event_store-eventbus-inmemory = { version = "0.1.0", path = "../event_store-eventbus-inmemory", optional = true } 25 | 26 | event_store-storage-postgres = { version = "0.1.0", path = "../event_store-storage-postgres", optional = true } 27 | event_store-backend-postgres = { version = "0.1.0", path = "../event_store-backend-postgres", optional = true } 28 | event_store-eventbus-postgres = { version = "0.1.0", path = "../event_store-eventbus-postgres", optional = true } 29 | 30 | [dev-dependencies] 31 | pretty_assertions = "1.0.0" 32 | test-log = { version = "0.2.8", default-features = false, features = ["trace"] } 33 | test-span = "0.1.1" 34 | tracing-subscriber = { version = "0.3", default-features = false, features = [ 35 | "env-filter", 36 | "fmt", 37 | ] } 38 | 39 | [features] 40 | default = ["postgres", "inmemory"] 41 | verbose = [] 42 | 43 | postgres = ["postgres_backend", "postgres_event_bus", "postgres_storage"] 44 | postgres_storage = ["event_store-storage-postgres"] 45 | postgres_backend = ["event_store-backend-postgres"] 46 | postgres_event_bus = ["event_store-eventbus-postgres"] 47 | 48 | inmemory = ["inmemory_backend", "inmemory_event_bus", "inmemory_storage"] 49 | inmemory_storage = ["event_store-storage-inmemory"] 50 | inmemory_backend = ["event_store-backend-inmemory"] 51 | inmemory_event_bus = ["event_store-eventbus-inmemory"] 52 | -------------------------------------------------------------------------------- /crates/event_store/src/connection/messaging.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | core::stream::Stream, event::RecordedEvent, event::UnsavedEvent, EventStoreError, 3 | ExpectedVersion, 4 | }; 5 | use actix::Message; 6 | use uuid::Uuid; 7 | 8 | #[derive(Message)] 9 | #[rtype(result = "Result")] 10 | pub struct StreamInfo { 11 | pub correlation_id: Uuid, 12 | pub stream_uuid: String, 13 | } 14 | 15 | #[derive(Message)] 16 | #[rtype(result = "Result")] 17 | pub struct StreamForward { 18 | pub correlation_id: Uuid, 19 | pub stream_uuid: String, 20 | } 21 | 22 | pub struct StreamForwardResult { 23 | pub stream: std::pin::Pin< 24 | Box, EventStoreError>> + Send>, 25 | >, 26 | } 27 | 28 | #[derive(Message)] 29 | #[rtype(result = "Result")] 30 | pub struct CreateStream { 31 | pub(crate) correlation_id: Uuid, 32 | pub(crate) stream_uuid: String, 33 | } 34 | 35 | #[derive(Message)] 36 | #[rtype(result = "Result, EventStoreError>")] 37 | pub struct Append { 38 | pub(crate) correlation_id: Uuid, 39 | pub(crate) stream: String, 40 | pub(crate) expected_version: ExpectedVersion, 41 | pub(crate) events: Vec, 42 | } 43 | 44 | #[derive(Message)] 45 | #[rtype(result = "Result, EventStoreError>")] 46 | pub struct Read { 47 | pub(crate) correlation_id: Uuid, 48 | pub(crate) stream: String, 49 | pub(crate) version: usize, 50 | pub(crate) limit: usize, 51 | } 52 | 53 | #[derive(Message)] 54 | #[rtype(result = "()")] 55 | pub struct StartPubSub {} 56 | -------------------------------------------------------------------------------- /crates/event_store/src/event/mod.rs: -------------------------------------------------------------------------------- 1 | use actix::Message; 2 | 3 | pub use crate::core::event::Event; 4 | pub use crate::core::event::RecordedEvent; 5 | pub use crate::core::event::UnsavedEvent; 6 | pub use crate::core::event::UnsavedEventError; 7 | 8 | #[derive(Debug, Clone, Message)] 9 | #[rtype(result = "()")] 10 | pub struct RecordedEvents { 11 | pub(crate) events: Vec, 12 | } 13 | -------------------------------------------------------------------------------- /crates/event_store/src/event_store/logic.rs: -------------------------------------------------------------------------------- 1 | use super::{EventStore, EventStoreBuilder}; 2 | use crate::{ 3 | connection::{ 4 | Append, Connection, CreateStream, Read, StreamForward, StreamForwardResult, StreamInfo, 5 | }, 6 | core::stream::Stream, 7 | event::RecordedEvent, 8 | prelude::EventStoreError, 9 | storage::{appender::AppendToStreamRequest, reader}, 10 | }; 11 | use actix::Addr; 12 | use event_store_core::storage::Storage; 13 | use tracing::info; 14 | use uuid::Uuid; 15 | 16 | impl std::default::Default for EventStore { 17 | fn default() -> Self { 18 | unimplemented!() 19 | } 20 | } 21 | 22 | impl EventStore { 23 | #[must_use] 24 | pub const fn builder() -> EventStoreBuilder { 25 | EventStoreBuilder { storage: None } 26 | } 27 | 28 | pub(crate) async fn read( 29 | connection: Addr>, 30 | request: reader::ReadStreamRequest, 31 | ) -> Result, EventStoreError> { 32 | let stream: String = request.stream.to_string(); 33 | 34 | info!("Attempting to read {} stream event(s)", stream); 35 | 36 | match connection 37 | .send(Read { 38 | correlation_id: request.correlation_id, 39 | #[cfg(feature = "verbose")] 40 | stream: stream.clone(), 41 | #[cfg(not(feature = "verbose"))] 42 | stream, 43 | version: request.version, 44 | limit: request.limit, 45 | }) 46 | .await? 47 | { 48 | Ok(events) => { 49 | #[cfg(feature = "verbose")] 50 | info!("Read {} event(s) to {}", events.len(), stream); 51 | Ok(events) 52 | } 53 | Err(e) => { 54 | #[cfg(feature = "verbose")] 55 | info!("Failed to read event(s) from {}", stream); 56 | Err(e) 57 | } 58 | } 59 | } 60 | 61 | pub(crate) async fn stream_info( 62 | connection: Addr>, 63 | request: StreamInfo, 64 | ) -> Result { 65 | connection.send(request).await? 66 | } 67 | 68 | pub(crate) async fn create_stream( 69 | connection: Addr>, 70 | request: CreateStream, 71 | ) -> Result { 72 | connection.send(request).await? 73 | } 74 | 75 | pub(crate) async fn stream_forward( 76 | connection: Addr>, 77 | request: StreamForward, 78 | ) -> Result { 79 | connection.send(request).await? 80 | } 81 | 82 | pub(crate) async fn append( 83 | connection: Addr>, 84 | request: AppendToStreamRequest, 85 | ) -> Result, EventStoreError> { 86 | let stream: String = request.stream.to_string(); 87 | 88 | #[cfg(feature = "verbose")] 89 | let events_number = request.events.len(); 90 | info!( 91 | "Attempting to append {} event(s) to {} with ExpectedVersion::{:?}", 92 | request.events.len(), 93 | request.stream, 94 | request.expected_version 95 | ); 96 | 97 | match connection 98 | .send(Append { 99 | correlation_id: request.correlation_id, 100 | #[cfg(feature = "verbose")] 101 | stream: stream.clone(), 102 | #[cfg(not(feature = "verbose"))] 103 | stream, 104 | expected_version: request.expected_version, 105 | events: request.events, 106 | }) 107 | .await? 108 | { 109 | Ok(events) => { 110 | #[cfg(feature = "verbose")] 111 | info!("Appended {} event(s) to {}", events.len(), stream); 112 | Ok(events) 113 | } 114 | Err(e) => { 115 | #[cfg(feature = "verbose")] 116 | info!("Failed to append {} event(s) to {}", events_number, stream); 117 | Err(e) 118 | } 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /crates/event_store/src/event_store/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::connection::Connection; 2 | pub use crate::event::Event; 3 | use actix::prelude::*; 4 | use event_store_core::{error::EventStoreError, storage::Storage}; 5 | use tracing::{instrument, trace}; 6 | 7 | mod logic; 8 | mod runtime; 9 | 10 | /// An `EventStore` that hold a storage connection 11 | #[derive(Debug, Clone)] 12 | pub struct EventStore { 13 | connection: Addr>, 14 | } 15 | 16 | /// Builder use to simplify the `EventStore` creation 17 | #[derive(Debug)] 18 | pub struct EventStoreBuilder { 19 | storage: Option, 20 | } 21 | 22 | impl EventStoreBuilder { 23 | /// Define which storage will be used by this building `EventStore` 24 | //FIXME: Unable to use `const` because of Option destruction 25 | #[allow(clippy::missing_const_for_fn)] 26 | pub fn storage(mut self, storage: S) -> Self { 27 | self.storage = Some(storage); 28 | 29 | self 30 | } 31 | 32 | /// Try to build the previously configured `EventStore` 33 | /// 34 | /// # Errors 35 | /// 36 | /// For now this method can fail only if you haven't define a `Storage` 37 | // #[instrument(level = "trace", name = "my_name", skip(self))] 38 | #[instrument(level = "trace", name = "EventStoreBuilder::build", skip(self))] 39 | pub async fn build(self) -> Result, EventStoreError> { 40 | self.storage.map_or_else( 41 | || Err(EventStoreError::NoStorage), 42 | |storage| { 43 | let connection = Connection::make(storage).start(); 44 | trace!("Creating EventStore with {} storage", S::storage_name()); 45 | 46 | Ok(EventStore { connection }) 47 | }, 48 | ) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /crates/event_store/src/event_store/runtime.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | connection::{CreateStream, StreamForward, StreamForwardResult, StreamInfo}, 3 | core::stream::Stream, 4 | event::RecordedEvent, 5 | prelude::EventStoreError, 6 | storage::{appender, reader}, 7 | }; 8 | 9 | use super::EventStore; 10 | use actix::prelude::*; 11 | use event_store_core::{event_bus::EventBusMessage, storage::Storage}; 12 | use tracing::{debug, Instrument}; 13 | use uuid::Uuid; 14 | 15 | impl Supervised for EventStore {} 16 | impl Actor for EventStore { 17 | type Context = ::actix::Context; 18 | 19 | #[tracing::instrument(name = "EventStore::Started", skip(self, _ctx))] 20 | fn started(&mut self, _ctx: &mut Self::Context) {} 21 | } 22 | 23 | impl StreamHandler for EventStore { 24 | fn handle(&mut self, item: EventBusMessage, _ctx: &mut Context) { 25 | debug!("EventBusMessage {:?}", item); 26 | } 27 | 28 | fn finished(&mut self, _ctx: &mut Self::Context) { 29 | debug!("finished"); 30 | } 31 | } 32 | 33 | impl Handler for EventStore { 34 | type Result = ResponseFuture, EventStoreError>>; 35 | 36 | #[tracing::instrument(name = "EventStore::ReadStreamRequest", skip(self, request, _ctx), fields(correlation_id = %request.correlation_id))] 37 | fn handle( 38 | &mut self, 39 | request: reader::ReadStreamRequest, 40 | _ctx: &mut Self::Context, 41 | ) -> Self::Result { 42 | let connection = self.connection.clone(); 43 | 44 | Box::pin(Self::read(connection, request).instrument(tracing::Span::current())) 45 | } 46 | } 47 | 48 | impl Handler for EventStore { 49 | type Result = ResponseFuture>; 50 | 51 | #[tracing::instrument(name = "EventStore::StreamInfo", skip(self, request, _ctx), fields(correlation_id = %request.correlation_id))] 52 | fn handle(&mut self, request: StreamInfo, _ctx: &mut Self::Context) -> Self::Result { 53 | debug!("Asking for stream {} infos", request.stream_uuid); 54 | 55 | Box::pin( 56 | Self::stream_info(self.connection.clone(), request) 57 | .instrument(tracing::Span::current()), 58 | ) 59 | } 60 | } 61 | 62 | impl Handler for EventStore { 63 | type Result = ResponseFuture>; 64 | 65 | #[tracing::instrument(name = "EventStore::CreateStream", skip(self, request, _ctx), fields(correlation_id = %request.correlation_id))] 66 | fn handle(&mut self, request: CreateStream, _ctx: &mut Self::Context) -> Self::Result { 67 | debug!("Creating stream {}", request.stream_uuid); 68 | 69 | Box::pin( 70 | Self::create_stream(self.connection.clone(), request) 71 | .instrument(tracing::Span::current()), 72 | ) 73 | } 74 | } 75 | 76 | impl Handler for EventStore { 77 | type Result = ResponseFuture, EventStoreError>>; 78 | #[tracing::instrument(name = "EventStore::AppendToStream", skip(self, request, _ctx), fields(correlation_id = %request.correlation_id))] 79 | fn handle( 80 | &mut self, 81 | request: appender::AppendToStreamRequest, 82 | _ctx: &mut Self::Context, 83 | ) -> Self::Result { 84 | Box::pin( 85 | Self::append(self.connection.clone(), request).instrument(tracing::Span::current()), 86 | ) 87 | } 88 | } 89 | 90 | impl Handler for EventStore { 91 | type Result = ResponseFuture>; 92 | 93 | fn handle(&mut self, request: StreamForward, _ctx: &mut Self::Context) -> Self::Result { 94 | Box::pin(Self::stream_forward(self.connection.clone(), request)) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /crates/event_store/src/prelude.rs: -------------------------------------------------------------------------------- 1 | pub use crate::connection::StreamForward; 2 | pub use crate::connection::StreamInfo; 3 | 4 | pub use crate::event::{Event, RecordedEvent, RecordedEvents, UnsavedEvent}; 5 | pub use crate::versions::{ExpectedVersion, ReadVersion}; 6 | pub use event_store_core::error::EventStoreError; 7 | 8 | #[cfg(feature = "inmemory_backend")] 9 | pub use event_store_backend_inmemory::InMemoryBackend; 10 | 11 | #[cfg(feature = "postgres_backend")] 12 | pub use event_store_backend_postgres::PostgresBackend; 13 | 14 | #[cfg(feature = "inmemory_event_bus")] 15 | pub use event_store_eventbus_inmemory::InMemoryEventBus; 16 | 17 | #[cfg(feature = "postgres_event_bus")] 18 | pub use event_store_eventbus_postgres::PostgresEventBus; 19 | 20 | #[cfg(feature = "inmemory_storage")] 21 | pub use crate::storage::InMemoryStorage; 22 | 23 | #[cfg(feature = "postgres_storage")] 24 | pub use crate::storage::PostgresStorage; 25 | 26 | pub use crate::storage::{appender::Appender, reader::Reader}; 27 | 28 | pub use crate::core::stream::Stream; 29 | 30 | pub use crate::subscriptions::StartFrom; 31 | pub use crate::subscriptions::Subscription; 32 | pub use crate::subscriptions::SubscriptionNotification; 33 | pub use crate::subscriptions::SubscriptionOptions; 34 | pub use crate::subscriptions::Subscriptions; 35 | pub use crate::subscriptions::SubscriptionsSupervisor; 36 | 37 | pub use crate::EventStore; 38 | -------------------------------------------------------------------------------- /crates/event_store/src/storage/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod appender; 2 | 3 | #[cfg(feature = "inmemory_storage")] 4 | pub use event_store_storage_inmemory::InMemoryStorage; 5 | 6 | #[cfg(feature = "postgres_storage")] 7 | pub use event_store_storage_postgres::PostgresStorage; 8 | 9 | pub mod reader; 10 | 11 | #[cfg(test)] 12 | mod test; 13 | -------------------------------------------------------------------------------- /crates/event_store/src/storage/reader.rs: -------------------------------------------------------------------------------- 1 | use crate::core::event::RecordedEvent; 2 | use crate::core::stream::Stream; 3 | use crate::EventStore; 4 | use crate::EventStoreError; 5 | use crate::ReadVersion; 6 | use event_store_core::storage::Storage; 7 | use tracing::trace; 8 | use uuid::Uuid; 9 | 10 | use actix::prelude::*; 11 | 12 | pub struct Reader { 13 | correlation_id: Uuid, 14 | span: tracing::Span, 15 | read_version: ReadVersion, 16 | stream: String, 17 | limit: usize, 18 | } 19 | 20 | impl Default for Reader { 21 | fn default() -> Self { 22 | Self::with_correlation_id(Uuid::new_v4()) 23 | } 24 | } 25 | 26 | impl Reader { 27 | #[tracing::instrument(name = "Reader")] 28 | pub fn with_correlation_id(correlation_id: Uuid) -> Self { 29 | let reader = Self { 30 | correlation_id, 31 | span: tracing::Span::current(), 32 | read_version: ReadVersion::Origin, 33 | stream: String::new(), 34 | limit: 1_000, 35 | }; 36 | 37 | trace!( 38 | parent: &reader.span, 39 | "Created"); 40 | 41 | reader 42 | } 43 | /// Define which stream we are reading from 44 | /// 45 | /// # Errors 46 | /// 47 | /// Can fail if the stream doesn't have the expected format 48 | pub fn stream(mut self, stream: &S) -> Result { 49 | // TODO: validate stream name format 50 | self.stream = stream.to_string(); 51 | 52 | trace!( 53 | parent: &self.span, 54 | "Defined stream {} as target", 55 | self.stream, 56 | ); 57 | 58 | Ok(self) 59 | } 60 | 61 | #[must_use] 62 | pub fn from(mut self, version: ReadVersion) -> Self { 63 | self.read_version = version; 64 | trace!( 65 | parent: &self.span, 66 | "Defined {:?}", self.read_version 67 | ); 68 | self 69 | } 70 | 71 | #[must_use] 72 | pub fn limit(mut self, limit: usize) -> Self { 73 | self.limit = limit; 74 | trace!( 75 | parent: &self.span, 76 | "Defined {:?} limit", self.limit); 77 | 78 | self 79 | } 80 | 81 | #[allow(clippy::missing_errors_doc)] 82 | pub async fn execute( 83 | self, 84 | event_store: Addr>, 85 | ) -> Result, EventStoreError> { 86 | trace!( 87 | parent: &self.span, 88 | "Attempting to execute"); 89 | 90 | if !Stream::validates_stream_id(&self.stream) { 91 | return Err(EventStoreError::InvalidStreamId); 92 | } 93 | 94 | event_store 95 | .send(ReadStreamRequest { 96 | correlation_id: self.correlation_id, 97 | span: tracing::span!(parent: &self.span, tracing::Level::TRACE, "ReadStreamRequest", correlation_id = ?self.correlation_id), 98 | stream: self.stream, 99 | version: match self.read_version { 100 | ReadVersion::Origin => 0, 101 | // FIXME: Update to use the correct type once sqlx is updated 102 | #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)] 103 | ReadVersion::Version(version) => version as usize 104 | }, 105 | limit: self.limit, 106 | }) 107 | .await? 108 | } 109 | } 110 | 111 | #[derive(Debug, Message)] 112 | #[rtype(result = "Result, EventStoreError>")] 113 | pub struct ReadStreamRequest { 114 | pub correlation_id: Uuid, 115 | pub span: tracing::Span, 116 | pub stream: String, 117 | pub version: usize, 118 | pub limit: usize, 119 | } 120 | -------------------------------------------------------------------------------- /crates/event_store/src/storage/test.rs: -------------------------------------------------------------------------------- 1 | use uuid::Uuid; 2 | 3 | use crate::core::stream::Stream; 4 | 5 | use std::str::FromStr; 6 | 7 | mod creation { 8 | use super::*; 9 | 10 | use event_store_backend_inmemory::InMemoryBackend; 11 | use event_store_core::{backend::Backend, storage::StorageError}; 12 | 13 | #[tokio::test] 14 | async fn success() { 15 | let mut storage = InMemoryBackend::default(); 16 | let uuid = Uuid::new_v4().to_string(); 17 | let c_id = Uuid::new_v4(); 18 | 19 | assert!(storage 20 | .create_stream(Stream::from_str(&uuid).unwrap(), c_id) 21 | .await 22 | .is_ok()); 23 | } 24 | 25 | #[tokio::test] 26 | async fn fail_if_stream_exists() { 27 | let mut storage = InMemoryBackend::default(); 28 | 29 | let uuid = Uuid::new_v4().to_string(); 30 | let c_id = Uuid::new_v4(); 31 | 32 | assert!(storage 33 | .create_stream(Stream::from_str(&uuid).unwrap(), c_id) 34 | .await 35 | .is_ok()); 36 | assert!(matches!( 37 | storage 38 | .create_stream(Stream::from_str(&uuid).unwrap(), c_id) 39 | .await, 40 | Err(StorageError::StreamAlreadyExists) 41 | )); 42 | } 43 | } 44 | 45 | mod deletion { 46 | use super::*; 47 | 48 | use event_store_backend_inmemory::InMemoryBackend; 49 | use event_store_core::{backend::Backend, storage::StorageError}; 50 | 51 | #[tokio::test] 52 | async fn success() { 53 | let mut storage = InMemoryBackend::default(); 54 | let uuid = Uuid::new_v4().to_string(); 55 | let c_id = Uuid::new_v4(); 56 | 57 | assert!(storage 58 | .create_stream(Stream::from_str(&uuid).unwrap(), c_id) 59 | .await 60 | .is_ok()); 61 | assert!(storage 62 | .delete_stream(&Stream::from_str(&uuid).unwrap(), c_id) 63 | .await 64 | .is_ok()); 65 | } 66 | 67 | #[tokio::test] 68 | async fn fail_if_stream_doesnt_exists() { 69 | let mut storage = InMemoryBackend::default(); 70 | 71 | let uuid = Uuid::new_v4().to_string(); 72 | let c_id = Uuid::new_v4(); 73 | 74 | assert!(matches!( 75 | storage 76 | .delete_stream(&Stream::from_str(&uuid).unwrap(), c_id) 77 | .await, 78 | Err(StorageError::StreamDoesntExists) 79 | )); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /crates/event_store/src/subscriptions/error.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug)] 2 | pub enum SubscriptionError { 3 | UnableToStart, 4 | } 5 | -------------------------------------------------------------------------------- /crates/event_store/src/subscriptions/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::event::RecordedEvent; 2 | use crate::EventStore; 3 | use actix::Addr; 4 | use actix::Message; 5 | use actix::Recipient; 6 | use event_store_core::storage::Storage; 7 | use std::borrow::Cow; 8 | use std::marker::PhantomData; 9 | use std::sync::Arc; 10 | use uuid::Uuid; 11 | 12 | mod error; 13 | mod fsm; 14 | pub mod pub_sub; 15 | mod state; 16 | mod subscriber; 17 | mod subscription; 18 | mod supervisor; 19 | 20 | #[cfg(test)] 21 | mod tests; 22 | 23 | use self::error::SubscriptionError; 24 | 25 | pub use self::subscription::Subscription; 26 | pub use supervisor::SubscriptionsSupervisor; 27 | 28 | /// 29 | /// Subscribe to a stream start a subscription in the supervisor 30 | /// The supervisor starts a subscription actor 31 | /// The subscription actor create a new FSM 32 | /// The subscription actor receive a Connect message 33 | /// - the FSM connects the subscriber 34 | /// - the FSM subscribe 35 | /// 36 | pub struct Subscriptions { 37 | _phantom: PhantomData, 38 | } 39 | 40 | #[derive(Debug, Clone)] 41 | pub struct SubscriptionOptions { 42 | pub stream_uuid: String, 43 | pub subscription_name: String, 44 | pub start_from: StartFrom, 45 | pub transient: bool, 46 | } 47 | 48 | impl Default for SubscriptionOptions { 49 | fn default() -> Self { 50 | Self { 51 | stream_uuid: String::new(), 52 | subscription_name: Uuid::new_v4().to_string(), 53 | start_from: StartFrom::default(), 54 | transient: false, 55 | } 56 | } 57 | } 58 | 59 | #[derive(Debug, PartialEq, Eq, Copy, Clone)] 60 | pub enum StartFrom { 61 | Origin, 62 | Version(u64), 63 | } 64 | 65 | impl From for u64 { 66 | fn from(s: StartFrom) -> Self { 67 | match s { 68 | StartFrom::Origin => 0, 69 | StartFrom::Version(i) => i, 70 | } 71 | } 72 | } 73 | impl Default for StartFrom { 74 | fn default() -> Self { 75 | Self::Origin 76 | } 77 | } 78 | 79 | #[derive(Debug, Message)] 80 | #[rtype(result = "Result<(), ()>")] 81 | pub enum SubscriptionNotification { 82 | Events(Arc>>), 83 | OwnedEvents(Cow<'static, Arc>>>), 84 | PubSubEvents(Arc, Vec>), 85 | Subscribed, 86 | } 87 | 88 | impl Subscriptions { 89 | #[allow(clippy::missing_errors_doc)] 90 | pub async fn subscribe_to_stream( 91 | subscriber: Recipient, 92 | options: SubscriptionOptions, 93 | storage: Addr>, 94 | ) -> Result>, SubscriptionError> 95 | where 96 | S: Storage, 97 | { 98 | let subscription = 99 | SubscriptionsSupervisor::::start_subscription(&options, storage).await?; 100 | let _ = Subscription::connect(&subscription, subscriber, &options).await; 101 | 102 | Ok(subscription) 103 | } 104 | 105 | pub fn notify_subscribers(stream_uuid: &str, events: Arc>>) { 106 | SubscriptionsSupervisor::::notify_subscribers(stream_uuid, events); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /crates/event_store/src/subscriptions/pub_sub.rs: -------------------------------------------------------------------------------- 1 | // TODO: PubSubNotification needs to be filterable by subscribtion (need to store a closure (RecordedEvent -> boolean)) 2 | use std::{collections::HashMap, sync::Arc}; 3 | 4 | use super::SubscriptionNotification; 5 | use actix::{Actor, Context, Handler, MailboxError, Message, Recipient, Supervised, SystemService}; 6 | use event_store_core::event::RecordedEvent; 7 | use tracing::trace; 8 | 9 | #[derive(Default, Debug)] 10 | pub struct PubSub { 11 | listeners: HashMap>>, 12 | } 13 | 14 | impl Actor for PubSub { 15 | type Context = Context; 16 | } 17 | 18 | impl SystemService for PubSub {} 19 | impl Supervised for PubSub {} 20 | 21 | impl PubSub { 22 | #[allow(clippy::missing_errors_doc)] 23 | #[allow(clippy::missing_panics_doc)] 24 | pub async fn subscribe(recipient: Recipient, stream: String) { 25 | Self::from_registry() 26 | .send(Subscribe(recipient, stream)) 27 | .await 28 | .unwrap(); 29 | } 30 | 31 | #[allow(clippy::missing_errors_doc)] 32 | pub async fn has_subscriber_for(stream: String) -> Result { 33 | Self::from_registry() 34 | .send(GetSubscriberCountForStream(stream)) 35 | .await 36 | } 37 | } 38 | 39 | #[derive(Message)] 40 | #[rtype(result = "()")] 41 | struct Subscribe(Recipient, String); 42 | 43 | #[derive(Message)] 44 | #[rtype(result = "usize")] 45 | struct GetSubscriberCountForStream(String); 46 | 47 | #[derive(Message)] 48 | #[rtype(result = "()")] 49 | pub struct PubSubNotification { 50 | pub(crate) stream: String, 51 | pub(crate) events: Vec, 52 | } 53 | 54 | impl Handler for PubSub { 55 | type Result = (); 56 | 57 | fn handle(&mut self, msg: Subscribe, _ctx: &mut Self::Context) -> Self::Result { 58 | self.listeners.entry(msg.1).or_default().push(msg.0); 59 | } 60 | } 61 | 62 | impl Handler for PubSub { 63 | type Result = usize; 64 | 65 | fn handle( 66 | &mut self, 67 | msg: GetSubscriberCountForStream, 68 | _ctx: &mut Self::Context, 69 | ) -> Self::Result { 70 | self.listeners.get(&msg.0).map_or(0, Vec::len) 71 | } 72 | } 73 | 74 | impl Handler for PubSub { 75 | type Result = (); 76 | 77 | fn handle(&mut self, msg: PubSubNotification, _ctx: &mut Self::Context) -> Self::Result { 78 | trace!("Received PubSubNotification"); 79 | // When receiving a PubSub notification 80 | // We need to: 81 | // - check if someone is listening for the stream OR if there is any $all listener 82 | // - convert the Vec into a Vec> 83 | // - Send Async notification to every $all / stream listeners 84 | 85 | let stream: Arc = Arc::new(msg.stream); 86 | let v: Vec> = msg.events.into_iter().map(Into::into).collect(); 87 | 88 | if let Some(listeners) = self.listeners.get::(&stream) { 89 | for listener in listeners.iter() { 90 | // FIX: Deal with failure 91 | let _ = listener.try_send(SubscriptionNotification::PubSubEvents( 92 | stream.clone(), 93 | v.clone(), 94 | )); 95 | } 96 | } 97 | 98 | // TODO: Group the two loops 99 | // WARNING: Listeners subscribing both to stream_id and $all will receive events 2times 100 | if let Some(listeners) = self.listeners.get::("$all") { 101 | for listener in listeners.iter() { 102 | // FIX: Deal with failure 103 | let _ = listener.try_send(SubscriptionNotification::PubSubEvents( 104 | stream.clone(), 105 | v.clone(), 106 | )); 107 | } 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /crates/event_store/src/subscriptions/state.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::VecDeque, sync::Arc}; 2 | 3 | use crate::{event::RecordedEvent, EventStore}; 4 | 5 | use super::{subscriber::Subscriber, StartFrom}; 6 | use actix::prelude::*; 7 | use event_store_core::storage::Storage; 8 | 9 | #[derive(Debug)] 10 | pub struct SubscriptionState { 11 | pub(crate) subscriber: Option, 12 | pub(crate) storage: Addr>, 13 | pub(crate) stream_uuid: String, 14 | pub(crate) start_from: StartFrom, 15 | pub(crate) subscription_name: String, 16 | pub(crate) last_received: u64, 17 | pub(crate) last_sent: u64, 18 | pub(crate) last_ack: u64, 19 | pub(crate) queue: VecDeque>, 20 | pub(crate) transient: bool, 21 | pub(crate) in_flight_event_numbers: Vec, 22 | } 23 | 24 | impl SubscriptionState { 25 | #[allow(clippy::unused_self)] 26 | pub(crate) fn reset_event_tracking(&mut self) {} 27 | } 28 | -------------------------------------------------------------------------------- /crates/event_store/src/subscriptions/subscriber.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::VecDeque, sync::Arc}; 2 | 3 | use actix::prelude::*; 4 | 5 | use crate::event::RecordedEvent; 6 | 7 | use super::SubscriptionNotification; 8 | 9 | #[derive(Debug)] 10 | pub struct Subscriber { 11 | pub recipient: Recipient, 12 | pub(crate) in_flight: VecDeque>, 13 | last_sent: u64, 14 | } 15 | 16 | impl Actor for Subscriber { 17 | type Context = Context; 18 | } 19 | 20 | impl Subscriber { 21 | pub(crate) fn with_recipient(recipient: Recipient) -> Self { 22 | Self { 23 | recipient, 24 | in_flight: VecDeque::default(), 25 | last_sent: 0, 26 | } 27 | } 28 | 29 | pub(crate) async fn notify_subscribed(&self) -> Result, MailboxError> { 30 | self.recipient 31 | .send(SubscriptionNotification::Subscribed) 32 | .await 33 | } 34 | 35 | pub(crate) fn track_in_flight(&mut self, event: Arc) { 36 | self.last_sent = event.event_number; 37 | self.in_flight.push_front(event); 38 | } 39 | 40 | pub(crate) async fn send_queued_events(&mut self) { 41 | let mut events: Vec<_> = self.in_flight.clone().into(); 42 | 43 | events.reverse(); 44 | 45 | // TODO: How to handle notification failure ? 46 | let _ = self 47 | .recipient 48 | .send(SubscriptionNotification::Events(Arc::new(events))) 49 | .await; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /crates/event_store/src/subscriptions/supervisor.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::marker::PhantomData; 3 | use std::sync::Arc; 4 | 5 | use crate::EventStore; 6 | 7 | use super::error::SubscriptionError; 8 | use super::subscription::Subscription; 9 | use super::SubscriptionOptions; 10 | use actix::prelude::*; 11 | use event_store_core::event::RecordedEvent; 12 | use event_store_core::storage::Storage; 13 | 14 | #[derive(Default, Debug)] 15 | pub struct SubscriptionsSupervisor { 16 | _storage: PhantomData, 17 | subscriptions: HashMap>>>, 18 | } 19 | 20 | impl Actor for SubscriptionsSupervisor { 21 | type Context = Context; 22 | } 23 | 24 | impl Supervised for SubscriptionsSupervisor {} 25 | impl ArbiterService for SubscriptionsSupervisor {} 26 | 27 | impl SubscriptionsSupervisor { 28 | #[allow(clippy::missing_errors_doc)] 29 | pub async fn start_subscription( 30 | options: &SubscriptionOptions, 31 | storage: Addr>, 32 | ) -> Result>, SubscriptionError> { 33 | Self::from_registry() 34 | .send(CreateSubscription(options.clone(), storage)) 35 | .await 36 | .map_err(|_| SubscriptionError::UnableToStart) 37 | } 38 | 39 | pub fn notify_subscribers(stream_uuid: &str, events: Arc>>) { 40 | Self::from_registry().do_send(Notify(stream_uuid.into(), events)); 41 | } 42 | } 43 | 44 | #[derive(Debug, Message, Clone)] 45 | #[rtype(result = "()")] 46 | pub struct Notify(pub(crate) String, pub(crate) Arc>>); 47 | 48 | #[derive(Debug, Message, Clone)] 49 | #[rtype(result = "()")] 50 | pub struct NotifySubscribers(pub(crate) String, pub(crate) Arc>>); 51 | 52 | impl Handler for SubscriptionsSupervisor { 53 | type Result = (); 54 | 55 | fn handle( 56 | &mut self, 57 | Notify(stream_uuid, events): Notify, 58 | _ctx: &mut Self::Context, 59 | ) -> Self::Result { 60 | if let Some(subscriptions) = self.subscriptions.get(&stream_uuid) { 61 | for sub in subscriptions { 62 | sub.do_send(NotifySubscribers(stream_uuid.clone(), events.clone())); 63 | } 64 | } 65 | } 66 | } 67 | 68 | #[derive(Message)] 69 | #[rtype(result = "Addr>")] 70 | struct CreateSubscription(SubscriptionOptions, Addr>); 71 | 72 | impl Handler> for SubscriptionsSupervisor { 73 | type Result = MessageResult>; 74 | 75 | fn handle(&mut self, msg: CreateSubscription, ctx: &mut Self::Context) -> Self::Result { 76 | let addr = Subscription::start_with_options(&msg.0, ctx.address(), msg.1); 77 | 78 | self.subscriptions 79 | .entry(msg.0.stream_uuid) 80 | .or_default() 81 | .push(addr.clone()); 82 | 83 | MessageResult(addr) 84 | } 85 | } 86 | 87 | #[derive(Message)] 88 | #[rtype(result = "()")] 89 | pub struct Started; 90 | 91 | impl Handler for SubscriptionsSupervisor { 92 | type Result = MessageResult; 93 | 94 | fn handle(&mut self, _: Started, _: &mut Self::Context) -> Self::Result { 95 | MessageResult(()) 96 | } 97 | } 98 | 99 | #[derive(Message)] 100 | #[rtype(result = "()")] 101 | pub struct GoingDown; 102 | 103 | impl Handler for SubscriptionsSupervisor { 104 | type Result = MessageResult; 105 | 106 | fn handle(&mut self, _: GoingDown, _: &mut Self::Context) -> Self::Result { 107 | MessageResult(()) 108 | } 109 | } 110 | 111 | #[derive(Message)] 112 | #[rtype(result = "()")] 113 | pub struct Down; 114 | 115 | impl Handler for SubscriptionsSupervisor { 116 | type Result = MessageResult; 117 | 118 | fn handle(&mut self, _: Down, _: &mut Self::Context) -> Self::Result { 119 | MessageResult(()) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /crates/event_store/src/subscriptions/tests/monitoring_subscription.rs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Freyskeyd/chekov/ac9966da79db26f4b1342107132c68c1cfd34dfe/crates/event_store/src/subscriptions/tests/monitoring_subscription.rs -------------------------------------------------------------------------------- /crates/event_store/src/subscriptions/tests/persistent_fsm.rs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Freyskeyd/chekov/ac9966da79db26f4b1342107132c68c1cfd34dfe/crates/event_store/src/subscriptions/tests/persistent_fsm.rs -------------------------------------------------------------------------------- /crates/event_store/src/subscriptions/tests/subscribe_to_stream.rs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Freyskeyd/chekov/ac9966da79db26f4b1342107132c68c1cfd34dfe/crates/event_store/src/subscriptions/tests/subscribe_to_stream.rs -------------------------------------------------------------------------------- /crates/event_store/src/subscriptions/tests/subscription_acknowledgement.rs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Freyskeyd/chekov/ac9966da79db26f4b1342107132c68c1cfd34dfe/crates/event_store/src/subscriptions/tests/subscription_acknowledgement.rs -------------------------------------------------------------------------------- /crates/event_store/src/subscriptions/tests/subscription_catch_up.rs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Freyskeyd/chekov/ac9966da79db26f4b1342107132c68c1cfd34dfe/crates/event_store/src/subscriptions/tests/subscription_catch_up.rs -------------------------------------------------------------------------------- /crates/event_store/src/subscriptions/tests/support/event.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryFrom; 2 | 3 | use event_store_core::event::{Event, RecordedEvent}; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | pub struct EventFactory {} 7 | 8 | impl EventFactory { 9 | pub const fn create_event(number: usize) -> TestEvent { 10 | TestEvent { event: number } 11 | } 12 | } 13 | 14 | #[derive(Serialize, Deserialize)] 15 | pub struct TestEvent { 16 | event: usize, 17 | } 18 | 19 | impl Event for TestEvent { 20 | fn event_type(&self) -> &'static str { 21 | "TestEvent" 22 | } 23 | 24 | fn all_event_types() -> Vec<&'static str> { 25 | vec!["TestEvent"] 26 | } 27 | } 28 | 29 | impl TryFrom for TestEvent { 30 | type Error = (); 31 | 32 | fn try_from(e: RecordedEvent) -> Result { 33 | serde_json::from_value(e.data).map_err(|_| ()) 34 | } 35 | } 36 | 37 | #[derive(serde::Serialize)] 38 | pub struct MyEvent {} 39 | impl Event for MyEvent { 40 | fn event_type(&self) -> &'static str { 41 | "MyEvent" 42 | } 43 | 44 | fn all_event_types() -> Vec<&'static str> { 45 | vec!["MyEvent"] 46 | } 47 | } 48 | 49 | impl TryFrom for MyEvent { 50 | type Error = (); 51 | 52 | fn try_from(_: RecordedEvent) -> Result { 53 | Ok(Self {}) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /crates/event_store/src/subscriptions/tests/support/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::VecDeque, sync::Arc}; 2 | 3 | use actix::{Actor, Addr, Context, Handler, ResponseFuture}; 4 | use event_store_core::{ 5 | error::EventStoreError, event::Event, storage::Storage, versions::ExpectedVersion, 6 | }; 7 | use futures::Future; 8 | use tokio::sync::Mutex; 9 | use uuid::Uuid; 10 | 11 | use crate::{subscriptions::SubscriptionNotification, EventStore}; 12 | 13 | pub mod event; 14 | pub mod subscriber; 15 | 16 | pub type Tracker = Arc>>; 17 | pub struct InnerSub { 18 | pub(crate) reference: Tracker, 19 | } 20 | 21 | impl Actor for InnerSub { 22 | type Context = Context; 23 | } 24 | 25 | impl Handler for InnerSub { 26 | type Result = ResponseFuture>; 27 | 28 | fn handle(&mut self, msg: SubscriptionNotification, _ctx: &mut Self::Context) -> Self::Result { 29 | let aquire = Arc::clone(&self.reference); 30 | 31 | Box::pin(async move { 32 | let mut mutex = aquire.lock().await; 33 | mutex.push_back(msg); 34 | 35 | Ok(()) 36 | }) 37 | } 38 | } 39 | 40 | pub struct EventStoreHelper { 41 | event_store: Addr>, 42 | } 43 | 44 | impl EventStoreHelper { 45 | pub(crate) async fn new(storage: T) -> Self { 46 | let event_store = EventStore::builder() 47 | .storage(storage) 48 | .build() 49 | .await 50 | .unwrap() 51 | .start(); 52 | 53 | Self { event_store } 54 | } 55 | 56 | pub(crate) fn append( 57 | &self, 58 | identity: &Uuid, 59 | version: ExpectedVersion, 60 | events: &[&E], 61 | ) -> impl Future, EventStoreError>> { 62 | let appender = crate::append() 63 | .to(identity) 64 | .unwrap() 65 | .expected_version(version) 66 | .events(events); 67 | 68 | let addr = self.get_addr(); 69 | async move { appender.unwrap().execute(addr).await } 70 | } 71 | 72 | pub(crate) fn get_addr(&self) -> Addr> { 73 | self.event_store.clone() 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /crates/event_store/src/subscriptions/tests/support/subscriber.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::VecDeque, sync::Arc}; 2 | 3 | use actix::{Actor, Addr}; 4 | use tokio::sync::Mutex; 5 | 6 | use crate::subscriptions::SubscriptionNotification; 7 | 8 | use super::{InnerSub, Tracker}; 9 | 10 | pub struct SubscriberFactory {} 11 | 12 | impl SubscriberFactory { 13 | pub fn setup() -> (Tracker, Addr) { 14 | let tracker: Arc>> = 15 | Arc::new(Mutex::new(VecDeque::new())); 16 | 17 | let addr = InnerSub { 18 | reference: Arc::clone(&tracker), 19 | } 20 | .start(); 21 | 22 | (tracker, addr) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /crates/event_store/src/subscriptions/tests/transient_fsm.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | prelude::ExpectedVersion, 3 | subscriptions::{ 4 | fsm::{InternalFSMState, SubscriptionFSM}, 5 | tests::support::{ 6 | event::{EventFactory, MyEvent}, 7 | subscriber::SubscriberFactory, 8 | EventStoreHelper, 9 | }, 10 | StartFrom, SubscriptionNotification, SubscriptionOptions, Subscriptions, 11 | }, 12 | InMemoryStorage, 13 | }; 14 | use serde_json::json; 15 | use test_log::test; 16 | use uuid::Uuid; 17 | 18 | #[test(actix::test)] 19 | async fn transient_subscription() { 20 | let es = EventStoreHelper::new(InMemoryStorage::default()).await; 21 | let identity = Uuid::new_v4(); 22 | 23 | let _ = es 24 | .append( 25 | &identity, 26 | ExpectedVersion::AnyVersion, 27 | &[&MyEvent {}, &MyEvent {}], 28 | ) 29 | .await; 30 | 31 | let (tracker, addr) = SubscriberFactory::setup(); 32 | let opts = SubscriptionOptions { 33 | transient: true, 34 | stream_uuid: identity.to_string(), 35 | ..Default::default() 36 | }; 37 | 38 | let mut fsm = SubscriptionFSM::with_options(&opts, es.get_addr()); 39 | 40 | assert_eq!(fsm.state, InternalFSMState::Initial); 41 | assert!(matches!(fsm.data.subscriber, None)); 42 | 43 | fsm.connect_subscriber(addr.recipient()).await; 44 | 45 | assert_eq!(fsm.state, InternalFSMState::Initial); 46 | assert!(matches!(fsm.data.subscriber, Some(_))); 47 | 48 | fsm.subscribe().await; 49 | 50 | let x = tracker.lock().await.pop_front(); 51 | assert!(matches!(x, Some(SubscriptionNotification::Subscribed))); 52 | 53 | assert_eq!(fsm.state, InternalFSMState::RequestCatchUp); 54 | assert_eq!(fsm.data.last_received, 0); 55 | assert_eq!(fsm.data.last_sent, 0); 56 | assert_eq!(fsm.data.last_ack, 0); 57 | 58 | fsm.catch_up().await; 59 | 60 | assert_eq!(fsm.state, InternalFSMState::CatchingUp); 61 | assert_eq!(fsm.data.subscriber.unwrap().in_flight.len(), 2); 62 | } 63 | 64 | #[test(actix::test)] 65 | async fn notify_subscribers_after_events_persisted_to_stream() { 66 | let es = EventStoreHelper::new(InMemoryStorage::default()).await; 67 | let identity = Uuid::new_v4(); 68 | 69 | let event = EventFactory::create_event(0); 70 | let (tracker, subscriber_addr) = SubscriberFactory::setup(); 71 | 72 | Subscriptions::subscribe_to_stream( 73 | subscriber_addr.recipient(), 74 | SubscriptionOptions { 75 | stream_uuid: identity.to_string(), 76 | subscription_name: identity.to_string(), 77 | start_from: StartFrom::Origin, 78 | transient: true, 79 | }, 80 | es.get_addr(), 81 | ) 82 | .await 83 | .expect("Unable to subscribe"); 84 | 85 | let x = tracker.lock().await.pop_front(); 86 | assert!(matches!(x, Some(SubscriptionNotification::Subscribed))); 87 | 88 | let _ = es 89 | .append(&identity, ExpectedVersion::AnyVersion, &[&event]) 90 | .await; 91 | 92 | let x = tracker.lock().await.pop_front(); 93 | assert!( 94 | matches!(x, Some(SubscriptionNotification::PubSubEvents(_, ref events)) if events.len() == 1) 95 | ); 96 | 97 | if let Some(SubscriptionNotification::PubSubEvents(stream_uuid, ref events)) = x { 98 | assert_eq!(stream_uuid.as_ref(), &identity.to_string()); 99 | assert_eq!(pluck!(events, event_number), [1]); 100 | assert_eq!(pluck!(events, stream_uuid), [identity.to_string()]); 101 | assert_eq!(pluck!(events, stream_version), [Some(1)]); 102 | assert_eq!(pluck!(events, correlation_id), [None]); 103 | assert_eq!(pluck!(events, causation_id), [None]); 104 | assert_eq!(pluck!(events, event_type), ["TestEvent".to_string()]); 105 | assert_eq!(pluck!(events, data), [json!({"event": 0})]); 106 | assert_eq!(pluck!(events, metadata), [None]); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /crates/watcher/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "watcher" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | uuid.workspace = true 10 | futures.workspace = true 11 | sqlx.workspace = true 12 | tokio.workspace = true 13 | 14 | event_store = { version = "0.1", path = "../event_store" } 15 | tui = "0.18" 16 | crossterm = "0.23" 17 | -------------------------------------------------------------------------------- /crates/watcher/src/app.rs: -------------------------------------------------------------------------------- 1 | use crossterm::event::{self, Event, KeyCode}; 2 | use std::{ 3 | io, 4 | sync::mpsc::{self, Sender}, 5 | thread, 6 | time::{Duration, Instant}, 7 | }; 8 | use tokio::runtime::Handle; 9 | use tui::{backend::Backend, Terminal}; 10 | use uuid::Uuid; 11 | 12 | use event_store::{ 13 | core::{backend::Backend as EventStoreBackend, event_bus::EventBus}, 14 | prelude::{PostgresBackend, PostgresEventBus, RecordedEvent}, 15 | }; 16 | use futures::TryStreamExt; 17 | 18 | use crate::{ui::ui, Message}; 19 | 20 | pub struct TabsState<'a> { 21 | pub titles: Vec<&'a str>, 22 | pub index: usize, 23 | } 24 | 25 | pub struct App<'a> { 26 | pub(crate) title: &'a str, 27 | pub(crate) tabs: TabsState<'a>, 28 | pub(crate) items: Vec, 29 | } 30 | 31 | impl<'a> App<'a> { 32 | pub(crate) fn new() -> App<'a> { 33 | App { 34 | title: "Chekov", 35 | tabs: TabsState { 36 | titles: vec!["Events", "Streams"], 37 | index: 0, 38 | }, 39 | items: vec![], 40 | } 41 | } 42 | } 43 | 44 | async fn run_stream(tx: Sender) -> Result<(), ()> { 45 | let backend = 46 | PostgresBackend::with_url("postgresql://postgres:postgres@localhost/event_store_bank") 47 | .await 48 | .unwrap(); 49 | 50 | let mut listener = PostgresEventBus::initiate( 51 | "postgresql://postgres:postgres@localhost/event_store_bank".to_string(), 52 | ) 53 | .await 54 | .unwrap(); 55 | 56 | let mut stream = listener.create_stream().await; 57 | while let Ok(Some(notif)) = stream.try_next().await { 58 | // Reload state 59 | 60 | match notif { 61 | event_store::core::event_bus::EventBusMessage::Notification(notification) 62 | if notification.stream_uuid == "$all" => 63 | { 64 | let stream_uuid = notification.stream_uuid; 65 | let correlation_id = Uuid::new_v4(); 66 | 67 | if let Ok(events) = backend 68 | .read_stream( 69 | stream_uuid.to_string(), 70 | notification.first_stream_version as usize, 71 | (notification.last_stream_version - notification.first_stream_version + 1) 72 | as usize, 73 | correlation_id, 74 | ) 75 | .await 76 | { 77 | let e = events 78 | .into_iter() 79 | .map(|event: RecordedEvent| { 80 | format!( 81 | "{event_type}({})", 82 | event.data, 83 | event_type = event.event_type 84 | ) 85 | }) 86 | .collect(); 87 | 88 | tx.send(Message::Event(e)).expect("Cannot send notif"); 89 | } 90 | } 91 | event_store::core::event_bus::EventBusMessage::Notification(_) => {} 92 | event_store::core::event_bus::EventBusMessage::Events(_stream, _events) => todo!(), 93 | event_store::core::event_bus::EventBusMessage::Unkown => {} 94 | } 95 | } 96 | Ok(()) 97 | } 98 | 99 | pub(crate) async fn run_app<'a, B: Backend>( 100 | terminal: &'a mut Terminal, 101 | mut app: App<'_>, 102 | ) -> io::Result<()> { 103 | let (tx, rx) = mpsc::channel(); 104 | let tick_rate = Duration::from_millis(200); 105 | let tx_one = tx.clone(); 106 | thread::spawn(move || { 107 | let mut last_tick = Instant::now(); 108 | loop { 109 | let timeout = tick_rate 110 | .checked_sub(last_tick.elapsed()) 111 | .unwrap_or_else(|| Duration::from_secs(0)); 112 | 113 | if event::poll(timeout).expect("poll works") { 114 | if let Event::Key(key) = event::read().expect("can read events") { 115 | if let KeyCode::Char('q') = key.code { 116 | tx_one.send(Message::Interrupt).expect("can send events"); 117 | } 118 | } 119 | } 120 | 121 | if last_tick.elapsed() >= tick_rate && tx_one.send(Message::Tick).is_ok() { 122 | last_tick = Instant::now(); 123 | } 124 | } 125 | }); 126 | 127 | let fut = run_stream(tx); 128 | let handle = Handle::current(); 129 | 130 | thread::spawn(move || { 131 | handle.spawn(fut); 132 | }); 133 | 134 | terminal.draw(|f| ui(f, &mut app))?; 135 | 136 | loop { 137 | match rx.try_recv() { 138 | Ok(Message::Interrupt) => { 139 | return Ok(()); 140 | } 141 | Ok(Message::Event(mut values)) => { 142 | app.items.append(&mut values); 143 | terminal.draw(|f| ui(f, &mut app))?; 144 | } 145 | _ => {} 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /crates/watcher/src/main.rs: -------------------------------------------------------------------------------- 1 | use app::{run_app, App}; 2 | use crossterm::{ 3 | event::{DisableMouseCapture, EnableMouseCapture}, 4 | execute, 5 | terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, 6 | }; 7 | use std::io; 8 | use tui::{backend::CrosstermBackend, Terminal}; 9 | 10 | mod app; 11 | mod ui; 12 | 13 | #[tokio::main] 14 | async fn main() -> Result<(), io::Error> { 15 | enable_raw_mode()?; 16 | let mut stdout = io::stdout(); 17 | execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; 18 | let backend = CrosstermBackend::new(stdout); 19 | let mut terminal = Terminal::new(backend)?; 20 | 21 | // create app and run it 22 | let app = App::new(); 23 | let res = run_app(&mut terminal, app).await; 24 | 25 | // restore terminal 26 | disable_raw_mode()?; 27 | execute!( 28 | terminal.backend_mut(), 29 | LeaveAlternateScreen, 30 | DisableMouseCapture 31 | )?; 32 | terminal.show_cursor()?; 33 | 34 | if let Err(err) = res { 35 | println!("{:?}", err) 36 | } 37 | 38 | Ok(()) 39 | } 40 | 41 | pub enum Message { 42 | Interrupt, 43 | Tick, 44 | Event(Vec), 45 | } 46 | -------------------------------------------------------------------------------- /crates/watcher/src/ui.rs: -------------------------------------------------------------------------------- 1 | use tui::{ 2 | backend::Backend, 3 | layout::{Constraint, Direction, Layout, Rect}, 4 | style::{Color, Modifier, Style}, 5 | text::{Span, Spans}, 6 | widgets::{Block, Borders, List, ListItem, ListState, Tabs}, 7 | Frame, 8 | }; 9 | 10 | use crate::app::App; 11 | mod util; 12 | use util::get_color; 13 | 14 | fn draw_selectable_list( 15 | f: &mut Frame, 16 | _app: &App, 17 | layout_chunk: Rect, 18 | title: &str, 19 | items: &[S], 20 | _highlight_state: (bool, bool), 21 | selected_index: Option, 22 | ) where 23 | B: Backend, 24 | S: std::convert::AsRef, 25 | { 26 | let mut state = ListState::default(); 27 | state.select(selected_index); 28 | 29 | let lst_items: Vec = items 30 | .iter() 31 | .map(|i| ListItem::new(Span::raw(i.as_ref()))) 32 | .collect(); 33 | 34 | let list = List::new(lst_items) 35 | .block( 36 | Block::default() 37 | .title(Span::styled(title, get_color())) 38 | .borders(Borders::ALL) 39 | .border_style(get_color()), 40 | ) 41 | .style(Style::default().fg(Color::Reset)) 42 | .highlight_style(get_color().add_modifier(Modifier::BOLD)); 43 | 44 | f.render_stateful_widget(list, layout_chunk, &mut state); 45 | } 46 | 47 | fn draw_main_tab(f: &mut Frame, app: &mut App, area: Rect) 48 | where 49 | B: Backend, 50 | { 51 | let constraints = vec![Constraint::Percentage(100)]; 52 | let chunks = Layout::default() 53 | .constraints(constraints) 54 | .direction(Direction::Horizontal) 55 | .split(area); 56 | { 57 | let chunks = Layout::default() 58 | .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) 59 | .direction(Direction::Horizontal) 60 | .split(chunks[0]); 61 | let logs = app.items.clone(); 62 | 63 | draw_selectable_list(f, app, chunks[0], "Events", &logs, (false, false), None) 64 | } 65 | } 66 | 67 | pub(crate) fn ui(f: &mut Frame, app: &mut App) { 68 | let rects = Layout::default() 69 | .constraints([Constraint::Length(3), Constraint::Min(0)].as_ref()) 70 | .split(f.size()); 71 | 72 | let titles = app 73 | .tabs 74 | .titles 75 | .iter() 76 | .map(|t| Spans::from(Span::styled(*t, Style::default().fg(Color::Green)))) 77 | .collect(); 78 | 79 | let tabs = Tabs::new(titles) 80 | .block(Block::default().borders(Borders::ALL).title(app.title)) 81 | .highlight_style(Style::default().fg(Color::Yellow)) 82 | .select(app.tabs.index); 83 | 84 | f.render_widget(tabs, rects[0]); 85 | 86 | if app.tabs.index == 0 { 87 | draw_main_tab(f, app, rects[1]) 88 | }; 89 | } 90 | -------------------------------------------------------------------------------- /crates/watcher/src/ui/util.rs: -------------------------------------------------------------------------------- 1 | use tui::style::{Color, Style}; 2 | 3 | pub fn get_color() -> Style { 4 | Style::default().fg(Color::Gray) 5 | } 6 | -------------------------------------------------------------------------------- /examples/bank/.env: -------------------------------------------------------------------------------- 1 | DATABASE_URL=postgresql://postgres:postgres@localhost:5432/bank 2 | -------------------------------------------------------------------------------- /examples/bank/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bank" 3 | version = "0.1.0" 4 | authors = ["Freyskeyd "] 5 | edition = "2018" 6 | workspace = "../../" 7 | 8 | [dependencies] 9 | actix.workspace = true 10 | futures.workspace = true 11 | serde.workspace = true 12 | serde_json.workspace = true 13 | sqlx.workspace = true 14 | tokio.workspace = true 15 | tracing.workspace = true 16 | uuid.workspace = true 17 | 18 | chekov = { path = "../../crates/chekov" } 19 | actix-web = "4.0.0-beta.9" 20 | tracing-subscriber = "0.2.24" 21 | -------------------------------------------------------------------------------- /examples/bank/src/account.rs: -------------------------------------------------------------------------------- 1 | use chekov::prelude::*; 2 | use serde::Serialize; 3 | use sqlx::PgPool; 4 | use uuid::Uuid; 5 | 6 | use crate::commands::*; 7 | use crate::events::*; 8 | mod aggregate; 9 | mod projector; 10 | mod repository; 11 | pub use aggregate::*; 12 | pub use projector::*; 13 | pub use repository::*; 14 | 15 | #[derive(Debug, Clone, PartialEq, Serialize)] 16 | pub enum AccountStatus { 17 | Initialized, 18 | Active, 19 | Deleted, 20 | } 21 | 22 | #[derive(Debug, Clone, chekov::Aggregate, Serialize)] 23 | #[aggregate(identity = "account")] 24 | pub struct Account { 25 | pub account_id: Option, 26 | pub name: String, 27 | pub status: AccountStatus, 28 | } 29 | 30 | impl std::default::Default for Account { 31 | fn default() -> Self { 32 | Self { 33 | account_id: None, 34 | name: String::new(), 35 | status: AccountStatus::Initialized, 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /examples/bank/src/account/aggregate.rs: -------------------------------------------------------------------------------- 1 | use std::{error::Error, fmt}; 2 | 3 | use super::*; 4 | 5 | use chekov::event::EventApplier; 6 | 7 | #[derive(Debug)] 8 | pub enum AccountError { 9 | UnableToCreate, 10 | } 11 | 12 | impl fmt::Display for AccountError { 13 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 14 | match self { 15 | AccountError::UnableToCreate => write!(f, "Can't open account"), 16 | } 17 | } 18 | } 19 | 20 | impl Error for AccountError {} 21 | 22 | impl CommandExecutor for Account { 23 | fn execute(cmd: DeleteAccount, _: &Self) -> Result, CommandExecutorError> { 24 | Ok(vec![AccountDeleted { 25 | account_id: cmd.account_id, 26 | }]) 27 | } 28 | } 29 | 30 | impl CommandExecutor for Account { 31 | fn execute(cmd: OpenAccount, state: &Self) -> Result, CommandExecutorError> { 32 | match state.status { 33 | AccountStatus::Initialized => Ok(vec![AccountOpened { 34 | account_id: cmd.account_id, 35 | name: cmd.name, 36 | }]), 37 | _ => Err(CommandExecutorError::ExecutionError(Box::new( 38 | AccountError::UnableToCreate, 39 | ))), 40 | } 41 | } 42 | } 43 | 44 | impl CommandExecutor for Account { 45 | fn execute( 46 | cmd: UpdateAccount, 47 | state: &Self, 48 | ) -> Result, CommandExecutorError> { 49 | Ok(vec![AccountUpdated::NameChanged( 50 | state.account_id.unwrap(), 51 | state.name.clone(), 52 | cmd.name, 53 | )]) 54 | } 55 | } 56 | 57 | #[chekov::applier] 58 | impl EventApplier for Account { 59 | fn apply(&mut self, event: &AccountOpened) -> Result<(), ApplyError> { 60 | self.account_id = Some(event.account_id); 61 | self.status = AccountStatus::Active; 62 | 63 | Ok(()) 64 | } 65 | } 66 | 67 | #[chekov::applier] 68 | impl EventApplier for Account { 69 | fn apply(&mut self, event: &AccountUpdated) -> Result<(), ApplyError> { 70 | if let AccountUpdated::NameChanged(_, _, new_name) = event { 71 | self.name = new_name.to_string(); 72 | } 73 | 74 | Ok(()) 75 | } 76 | } 77 | 78 | #[chekov::applier] 79 | impl EventApplier for Account { 80 | fn apply(&mut self, _: &AccountDeleted) -> Result<(), ApplyError> { 81 | self.status = AccountStatus::Deleted; 82 | 83 | Ok(()) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /examples/bank/src/account/projector.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use chekov::error::HandleError; 3 | use futures::{future::BoxFuture, FutureExt}; 4 | 5 | #[derive(chekov::EventHandler, Clone)] 6 | pub struct AccountProjector { 7 | pub pool: PgPool, 8 | } 9 | 10 | #[chekov::event_handler] 11 | impl chekov::event::Handler for AccountProjector { 12 | fn handle(&mut self, event: &AccountOpened) -> BoxFuture> { 13 | let event = event.clone(); 14 | let pool = self.pool.acquire(); 15 | async move { 16 | let p = pool.await.unwrap(); 17 | let _result = AccountRepository::create(&event, p).await; 18 | 19 | Ok(()) 20 | } 21 | .boxed() 22 | } 23 | } 24 | 25 | #[chekov::event_handler] 26 | impl chekov::event::Handler for AccountProjector { 27 | fn handle(&mut self, event: &AccountUpdated) -> BoxFuture> { 28 | let pool = self.pool.acquire(); 29 | let event = event.clone(); 30 | 31 | async move { 32 | if let Ok(p) = pool.await { 33 | if let AccountUpdated::NameChanged(account_id, _, name) = event { 34 | let _result = AccountRepository::update(&account_id, &name, p).await; 35 | } 36 | } 37 | 38 | Ok(()) 39 | } 40 | .boxed() 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /examples/bank/src/account/repository.rs: -------------------------------------------------------------------------------- 1 | use super::{Account, AccountStatus}; 2 | use sqlx::postgres::PgRow; 3 | use sqlx::{Acquire, Row}; 4 | use uuid::Uuid; 5 | 6 | use crate::events::*; 7 | 8 | pub struct AccountRepository {} 9 | 10 | impl AccountRepository { 11 | pub async fn find_all( 12 | mut pool: sqlx::pool::PoolConnection, 13 | ) -> Result, sqlx::Error> { 14 | sqlx::query( 15 | r#" 16 | SELECT account_id, name 17 | FROM accounts 18 | ORDER BY account_id 19 | "#, 20 | ) 21 | .map(|row: PgRow| Account { 22 | account_id: Some(row.get(0)), 23 | name: row.get(1), 24 | status: AccountStatus::Initialized, 25 | }) 26 | .fetch_all(&mut pool) 27 | .await 28 | } 29 | 30 | pub async fn create( 31 | account: &AccountOpened, 32 | mut pool: sqlx::pool::PoolConnection, 33 | ) -> Result { 34 | let mut tx = pool.begin().await?; 35 | let todo = sqlx::query( 36 | "INSERT INTO accounts (account_id, name) VALUES ($1, $2) RETURNING account_id, name", 37 | ) 38 | .bind(account.account_id) 39 | .bind(&account.name) 40 | .map(|row: PgRow| Account { 41 | account_id: row.get(0), 42 | name: row.get(1), 43 | status: AccountStatus::Active, 44 | }) 45 | .fetch_one(&mut tx) 46 | .await?; 47 | 48 | tx.commit().await?; 49 | Ok(todo) 50 | } 51 | 52 | pub async fn update( 53 | account_id: &Uuid, 54 | name: &str, 55 | mut pool: sqlx::pool::PoolConnection, 56 | ) -> Result { 57 | let mut tx = pool.begin().await?; 58 | let todo = sqlx::query( 59 | "UPDATE accounts SET name = $1 WHERE account_id = $2 RETURNING account_id, name", 60 | ) 61 | .bind(name) 62 | .bind(account_id) 63 | .map(|row: PgRow| Account { 64 | account_id: row.get(0), 65 | name: row.get(1), 66 | status: AccountStatus::Active, 67 | }) 68 | .fetch_one(&mut tx) 69 | .await?; 70 | 71 | tx.commit().await?; 72 | Ok(todo) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /examples/bank/src/commands.rs: -------------------------------------------------------------------------------- 1 | use chekov::aggregate::StaticState; 2 | use chekov::prelude::*; 3 | use futures::future::BoxFuture; 4 | use futures::FutureExt; 5 | use serde::Deserialize; 6 | use serde::Serialize; 7 | use uuid::Uuid; 8 | 9 | use crate::account::*; 10 | use crate::events::*; 11 | 12 | #[derive(Debug)] 13 | pub struct DeleteAccount { 14 | pub account_id: Uuid, 15 | } 16 | 17 | impl Command for DeleteAccount { 18 | type Event = AccountDeleted; 19 | type Executor = Account; 20 | type ExecutorRegistry = AggregateInstanceRegistry; 21 | type CommandHandler = NoHandler; 22 | 23 | fn identifier(&self) -> ::std::string::String { 24 | self.account_id.to_string() 25 | } 26 | } 27 | 28 | #[derive(chekov::Command, Serialize, Deserialize)] 29 | #[command( 30 | event = "AccountOpened", 31 | aggregate = "Account", 32 | handler = "AccountValidator" 33 | )] 34 | pub struct OpenAccount { 35 | #[command(identifier)] 36 | pub account_id: Uuid, 37 | pub name: String, 38 | } 39 | 40 | #[derive(Clone, Debug, chekov::Command, Serialize, Deserialize)] 41 | #[command(event = "AccountUpdated", aggregate = "Account")] 42 | pub struct UpdateAccount { 43 | #[command(identifier)] 44 | pub account_id: Uuid, 45 | pub name: String, 46 | } 47 | 48 | #[derive(Default, CommandHandler)] 49 | pub struct AccountValidator {} 50 | 51 | #[chekov::command_handler] 52 | impl chekov::command::Handler for AccountValidator { 53 | fn handle( 54 | &mut self, 55 | command: OpenAccount, 56 | state: StaticState, 57 | ) -> BoxFuture<'static, Result, CommandExecutorError>> { 58 | async move { 59 | if state.status != AccountStatus::Initialized { 60 | Err(CommandExecutorError::ExecutionError(Box::new( 61 | AccountError::UnableToCreate, 62 | ))) 63 | } else { 64 | Account::execute(command, &state) 65 | } 66 | } 67 | .boxed() 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /examples/bank/src/events.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | use serde::Serialize; 3 | use uuid::Uuid; 4 | 5 | #[derive(Clone, chekov::Event, Deserialize, Serialize)] 6 | pub struct AccountDeleted { 7 | pub account_id: Uuid, 8 | } 9 | 10 | #[derive(Clone, chekov::Event, Deserialize, Serialize)] 11 | pub struct AccountOpened { 12 | pub account_id: Uuid, 13 | pub name: String, 14 | } 15 | 16 | #[derive(Debug, Clone, chekov::Event, Deserialize, Serialize)] 17 | pub enum AccountUpdated { 18 | NameChanged(Uuid, String, String), 19 | Deleted, 20 | Forced { why: String }, 21 | Disabled(String), 22 | } 23 | 24 | #[derive(Clone, chekov::Event, Deserialize, Serialize)] 25 | #[event(event_type = "Elixir.Conduit.Accounts.Events.UserRegistered")] 26 | pub struct UserRegistered { 27 | pub email: String, 28 | pub hashed_password: String, 29 | pub user_uuid: Uuid, 30 | pub username: String, 31 | } 32 | 33 | #[derive(Clone, chekov::Event, Deserialize, Serialize)] 34 | #[event(event_type = "MoneyMovement")] 35 | pub enum MoneyMovementEvent { 36 | Deposited { 37 | account_id: Uuid, 38 | }, 39 | Withdrawn { 40 | account_id: Uuid, 41 | }, 42 | #[event(event_type = "MoneyDeleted")] 43 | Removed, 44 | Added(String), 45 | } 46 | -------------------------------------------------------------------------------- /examples/bank/src/http.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use crate::account::*; 3 | use crate::commands::*; 4 | use actix_web::body::BoxBody; 5 | use actix_web::web; 6 | use actix_web::{delete, get, post, put}; 7 | use actix_web::{HttpRequest, HttpResponse, Responder}; 8 | use serde_json::json; 9 | use sqlx::PgPool; 10 | use uuid::Uuid; 11 | 12 | impl Responder for Account { 13 | type Body = BoxBody; 14 | 15 | fn respond_to(self, _req: &HttpRequest) -> HttpResponse { 16 | let body = serde_json::to_string(&self).unwrap(); 17 | 18 | HttpResponse::Ok() 19 | .content_type("application/json") 20 | .body(body) 21 | } 22 | } 23 | 24 | #[get("/accounts")] 25 | pub async fn find_all(db_pool: web::Data) -> impl Responder { 26 | let result = AccountRepository::find_all(db_pool.acquire().await.unwrap()).await; 27 | match result { 28 | Ok(todos) => HttpResponse::Ok().json(todos), 29 | _ => HttpResponse::BadRequest().body("Error trying to read all todos from database"), 30 | } 31 | } 32 | #[get("/accounts/{id}")] 33 | pub async fn find(_id: web::Path) -> impl Responder { 34 | HttpResponse::InternalServerError().body("Unimplemented") 35 | } 36 | 37 | #[post("/accounts")] 38 | pub async fn create(account: web::Json) -> impl Responder { 39 | match Router::::dispatch(account.into_inner(), CommandMetadatas::default()).await { 40 | Ok(res) => HttpResponse::Ok().json(res.first()), // <- send response 41 | Err(e) => HttpResponse::Ok().json(json!({ "error": e.to_string() })), 42 | } 43 | } 44 | 45 | #[put("/accounts/{id}")] 46 | pub async fn update( 47 | account_id: web::Path, 48 | account: web::Json, 49 | ) -> impl Responder { 50 | match Router::::dispatch( 51 | UpdateAccount { 52 | account_id: account_id.into_inner(), 53 | name: account.name.to_string(), 54 | }, 55 | CommandMetadatas::default(), 56 | ) 57 | .await 58 | { 59 | Ok(res) => HttpResponse::Ok().json(res.first()), // <- send response 60 | Err(e) => HttpResponse::Ok().json(json!({ "error": e.to_string() })), 61 | } 62 | } 63 | 64 | #[delete("/accounts/{id}")] 65 | pub async fn delete(id: web::Path) -> impl Responder { 66 | match Router::::dispatch( 67 | DeleteAccount { 68 | account_id: id.into_inner(), 69 | }, 70 | CommandMetadatas::default(), 71 | ) 72 | .await 73 | { 74 | Ok(res) => HttpResponse::Ok().json(res), 75 | Err(e) => HttpResponse::Ok().json(json!({ "error": e.to_string() })), 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /examples/bank/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | 3 | use actix_web::{ 4 | middleware, 5 | web::{self, Data}, 6 | App, HttpServer, 7 | }; 8 | use chekov::prelude::*; 9 | use sqlx::PgPool; 10 | 11 | mod account; 12 | mod commands; 13 | mod events; 14 | mod http; 15 | 16 | pub fn init(cfg: &mut web::ServiceConfig) { 17 | cfg.service(http::find_all); 18 | cfg.service(http::find); 19 | cfg.service(http::create); 20 | cfg.service(http::update); 21 | cfg.service(http::delete); 22 | } 23 | 24 | #[derive(Default)] 25 | struct DefaultApp {} 26 | 27 | impl Application for DefaultApp { 28 | type Storage = PostgresStorage; 29 | } 30 | 31 | #[actix::main] 32 | async fn main() -> std::io::Result<()> { 33 | tracing_subscriber::fmt::init(); 34 | 35 | let db_pool = PgPool::connect("postgresql://postgres:postgres@localhost/bank") 36 | .await 37 | .unwrap(); 38 | 39 | // Configure the storage (PG, InMemory,...) 40 | DefaultApp::with_default() 41 | .event_handler(account::AccountProjector { 42 | pool: db_pool.clone(), 43 | }) 44 | .storage(PostgresStorage::with_url( 45 | "postgresql://postgres:postgres@localhost/event_store_bank", 46 | )) 47 | .launch() 48 | .await; 49 | 50 | HttpServer::new(move || { 51 | App::new() 52 | .wrap(middleware::Logger::default()) 53 | .app_data(Data::new(db_pool.clone())) 54 | .app_data(web::JsonConfig::default().limit(4096)) 55 | .configure(init) 56 | }) 57 | .bind(env::var("RUNTIME_ENDPOINT").expect("Must define the RUNTIME_ENDPOINT"))? 58 | .run() 59 | .await?; 60 | 61 | tokio::signal::ctrl_c().await.unwrap(); 62 | println!("Ctrl-C received, shutting down"); 63 | Ok(()) 64 | } 65 | -------------------------------------------------------------------------------- /examples/gift_shop/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gift_shop" 3 | version = "0.1.0" 4 | edition = "2018" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | uuid.workspace = true 10 | serde.workspace = true 11 | serde_json.workspace = true 12 | futures.workspace = true 13 | tokio.workspace = true 14 | sqlx.workspace = true 15 | actix.workspace = true 16 | 17 | chekov = { path = "../../crates/chekov" } 18 | actix-web = "4.0.0-beta.9" 19 | tracing-subscriber = "0.2.24" 20 | -------------------------------------------------------------------------------- /examples/gift_shop/src/account.rs: -------------------------------------------------------------------------------- 1 | use chekov::prelude::*; 2 | use serde::Serialize; 3 | use sqlx::PgPool; 4 | use uuid::Uuid; 5 | 6 | use crate::commands::*; 7 | 8 | mod aggregate; 9 | mod projector; 10 | mod repository; 11 | 12 | pub use aggregate::*; 13 | pub use projector::*; 14 | pub use repository::*; 15 | 16 | #[derive(Debug, Clone, PartialEq, Serialize)] 17 | pub enum AccountStatus { 18 | Initialized, 19 | Active, 20 | } 21 | 22 | #[derive(Debug, Clone, chekov::Aggregate, Serialize)] 23 | #[aggregate(identity = "account")] 24 | pub struct Account { 25 | pub account_id: Option, 26 | pub name: String, 27 | pub status: AccountStatus, 28 | pub balance: i64, 29 | } 30 | 31 | impl std::default::Default for Account { 32 | fn default() -> Self { 33 | Self { 34 | account_id: None, 35 | name: String::new(), 36 | status: AccountStatus::Initialized, 37 | balance: 0, 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /examples/gift_shop/src/account/aggregate.rs: -------------------------------------------------------------------------------- 1 | use std::{error::Error, fmt}; 2 | 3 | use super::*; 4 | use crate::events::account::*; 5 | use chekov::event::EventApplier; 6 | 7 | #[derive(Debug)] 8 | enum AccountError { 9 | UnableToCreate, 10 | } 11 | 12 | impl fmt::Display for AccountError { 13 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 14 | match self { 15 | AccountError::UnableToCreate => write!(f, "Can't open account"), 16 | } 17 | } 18 | } 19 | 20 | impl Error for AccountError {} 21 | 22 | impl CommandExecutor for Account { 23 | fn execute(cmd: OpenAccount, state: &Self) -> Result, CommandExecutorError> { 24 | match state.status { 25 | AccountStatus::Initialized => Ok(vec![AccountOpened { 26 | account_id: cmd.account_id, 27 | name: cmd.name, 28 | balance: 0, 29 | }]), 30 | _ => Err(CommandExecutorError::ExecutionError(Box::new( 31 | AccountError::UnableToCreate, 32 | ))), 33 | } 34 | } 35 | } 36 | 37 | #[chekov::applier] 38 | impl EventApplier for Account { 39 | fn apply(&mut self, event: &AccountOpened) -> Result<(), ApplyError> { 40 | self.account_id = Some(event.account_id); 41 | self.status = AccountStatus::Active; 42 | 43 | Ok(()) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /examples/gift_shop/src/account/projector.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use crate::events::account::*; 3 | use chekov::error::HandleError; 4 | use futures::{future::BoxFuture, FutureExt}; 5 | 6 | #[derive(chekov::EventHandler, Clone)] 7 | pub struct AccountProjector { 8 | pub pool: PgPool, 9 | } 10 | 11 | #[chekov::event_handler] 12 | impl chekov::event::Handler for AccountProjector { 13 | fn handle(&mut self, event: &AccountOpened) -> BoxFuture> { 14 | let event = event.clone(); 15 | let pool = self.pool.acquire(); 16 | async move { 17 | let p = pool.await.unwrap(); 18 | let _result = AccountRepository::create(&event, p).await; 19 | 20 | Ok(()) 21 | } 22 | .boxed() 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /examples/gift_shop/src/account/repository.rs: -------------------------------------------------------------------------------- 1 | use super::{Account, AccountStatus}; 2 | use sqlx::postgres::PgRow; 3 | use sqlx::{Acquire, Row}; 4 | 5 | use crate::events::*; 6 | 7 | pub struct AccountRepository {} 8 | 9 | impl AccountRepository { 10 | pub async fn create( 11 | account: &account::AccountOpened, 12 | mut pool: sqlx::pool::PoolConnection, 13 | ) -> Result { 14 | let mut tx = pool.begin().await?; 15 | let todo = sqlx::query( 16 | "INSERT INTO accounts (account_id, name) VALUES ($1, $2) RETURNING account_id, name, balance", 17 | ) 18 | .bind(account.account_id) 19 | .bind(&account.name) 20 | .map(|row: PgRow| Account { 21 | account_id: row.get(0), 22 | name: row.get(1), 23 | status: AccountStatus::Active, 24 | balance: row.get::(2), 25 | }) 26 | .fetch_one(&mut tx) 27 | .await?; 28 | 29 | tx.commit().await?; 30 | Ok(todo) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /examples/gift_shop/src/commands.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | use serde::Serialize; 3 | use uuid::Uuid; 4 | 5 | use crate::account::*; 6 | use crate::events::*; 7 | use crate::gift_card::*; 8 | use crate::order::*; 9 | 10 | #[derive(chekov::Command, Serialize, Deserialize)] 11 | #[command(event = "account::AccountOpened", aggregate = "Account")] 12 | pub struct OpenAccount { 13 | #[command(identifier)] 14 | pub account_id: Uuid, 15 | pub name: String, 16 | } 17 | 18 | #[derive(chekov::Command, Serialize, Deserialize)] 19 | #[command(event = "gift_card::GiftCardCreated", aggregate = "GiftCard")] 20 | pub struct CreateGiftCard { 21 | #[command(identifier)] 22 | pub gift_card_id: Uuid, 23 | pub name: String, 24 | pub price: i64, 25 | pub count: usize, 26 | } 27 | 28 | #[derive(Clone, chekov::Command, Deserialize, Serialize)] 29 | #[command(event = "order::OrderCreated", aggregate = "Order")] 30 | pub struct CreateOrder { 31 | #[command(identifier)] 32 | pub order_id: Uuid, 33 | pub account_id: Uuid, 34 | } 35 | 36 | #[derive(Clone, chekov::Command, Deserialize, Serialize)] 37 | #[command(event = "order::GiftCardAdded", aggregate = "Order")] 38 | pub struct AddGiftCardToOrder { 39 | #[command(identifier)] 40 | pub order_id: Uuid, 41 | pub gift_card_id: Uuid, 42 | pub amount: usize, 43 | pub price: i64, 44 | } 45 | 46 | #[derive(Clone, chekov::Command, Deserialize, Serialize)] 47 | #[command(event = "order::OrderValidated", aggregate = "Order")] 48 | pub struct ValidateOrder { 49 | #[command(identifier)] 50 | pub order_id: Uuid, 51 | } 52 | 53 | #[derive(Clone, chekov::Command, Deserialize, Serialize)] 54 | #[command(event = "order::OrderCanceled", aggregate = "Order")] 55 | pub struct CancelOrder { 56 | #[command(identifier)] 57 | pub order_id: Uuid, 58 | } 59 | -------------------------------------------------------------------------------- /examples/gift_shop/src/events.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | use serde::Serialize; 3 | use uuid::Uuid; 4 | 5 | pub(crate) mod account { 6 | use super::*; 7 | 8 | #[derive(Clone, chekov::Event, Deserialize, Serialize)] 9 | pub struct AccountOpened { 10 | pub account_id: Uuid, 11 | pub name: String, 12 | pub balance: i64, 13 | } 14 | 15 | #[derive(Clone, chekov::Event, Deserialize, Serialize)] 16 | #[event(event_type = "MoneyMovement")] 17 | pub enum MoneyMovementEvent { 18 | Deposited { account_id: Uuid, amount: u64 }, 19 | Withdrawn { account_id: Uuid, amount: u64 }, 20 | } 21 | } 22 | 23 | pub(crate) mod gift_card { 24 | use super::*; 25 | 26 | #[derive(Clone, chekov::Event, Deserialize, Serialize)] 27 | pub struct GiftCardCreated { 28 | pub gift_card_id: Uuid, 29 | pub name: String, 30 | pub price: i64, 31 | pub count: i16, 32 | } 33 | 34 | #[derive(Clone, chekov::Event, Deserialize, Serialize)] 35 | pub struct GiftCardUsed { 36 | pub gift_card_id: Uuid, 37 | pub account_id: Uuid, 38 | } 39 | } 40 | 41 | pub(crate) mod order { 42 | use std::collections::HashMap; 43 | 44 | use crate::order::Item; 45 | 46 | use super::*; 47 | 48 | #[derive(Clone, chekov::Event, Deserialize, Serialize)] 49 | pub struct OrderCreated { 50 | pub order_id: Uuid, 51 | pub account_id: Uuid, 52 | } 53 | 54 | #[derive(Clone, chekov::Event, Deserialize, Serialize)] 55 | pub struct GiftCardAdded { 56 | pub order_id: Uuid, 57 | pub gift_card_id: Uuid, 58 | pub amount: usize, 59 | pub price: i64, 60 | } 61 | 62 | #[derive(Clone, chekov::Event, Deserialize, Serialize)] 63 | pub struct OrderValidated { 64 | pub order_id: Uuid, 65 | pub account_id: Uuid, 66 | pub items: HashMap, 67 | pub total_price: i64, 68 | } 69 | 70 | #[derive(Clone, chekov::Event, Deserialize, Serialize)] 71 | pub struct OrderCanceled { 72 | pub order_id: Uuid, 73 | pub account_id: Uuid, 74 | pub total_price: i64, 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /examples/gift_shop/src/gift_card.rs: -------------------------------------------------------------------------------- 1 | use chekov::error::HandleError; 2 | use chekov::prelude::*; 3 | use futures::future::BoxFuture; 4 | use futures::FutureExt; 5 | use serde::Serialize; 6 | use sqlx::postgres::PgRow; 7 | use sqlx::{Acquire, PgPool, Row}; 8 | use uuid::Uuid; 9 | 10 | use crate::commands::*; 11 | use crate::events::gift_card::GiftCardCreated; 12 | 13 | #[derive(Clone, Debug, PartialEq, Serialize)] 14 | pub enum GiftCardState { 15 | Unknown, 16 | Created, 17 | } 18 | 19 | #[derive(Default, Debug, Clone, chekov::Aggregate, Serialize)] 20 | #[aggregate(identity = "gift_card")] 21 | pub struct GiftCard { 22 | pub gift_card_id: Option, 23 | pub name: String, 24 | pub price: i64, 25 | pub count: i32, 26 | pub gift_card_state: GiftCardState, 27 | } 28 | 29 | impl Default for GiftCardState { 30 | fn default() -> Self { 31 | Self::Unknown 32 | } 33 | } 34 | 35 | impl CommandExecutor for GiftCard { 36 | fn execute(cmd: CreateGiftCard, state: &Self) -> ExecutionResult { 37 | let events = if state.gift_card_state == GiftCardState::Unknown { 38 | vec![GiftCardCreated { 39 | gift_card_id: cmd.gift_card_id, 40 | name: cmd.name, 41 | price: cmd.price, 42 | count: cmd.count as i16, 43 | }] 44 | } else { 45 | vec![] 46 | }; 47 | 48 | Ok(events) 49 | } 50 | } 51 | 52 | #[chekov::applier] 53 | impl EventApplier for GiftCard { 54 | fn apply(&mut self, event: &GiftCardCreated) -> Result<(), ApplyError> { 55 | self.gift_card_state = GiftCardState::Created; 56 | self.gift_card_id = Some(event.gift_card_id); 57 | self.name = event.name.clone(); 58 | self.price = event.price; 59 | self.count = event.count as i32; 60 | 61 | Ok(()) 62 | } 63 | } 64 | 65 | #[derive(chekov::EventHandler, Clone)] 66 | pub struct GiftCardProjector { 67 | pub pool: PgPool, 68 | } 69 | 70 | #[chekov::event_handler] 71 | impl chekov::event::Handler for GiftCardProjector { 72 | fn handle(&mut self, event: &GiftCardCreated) -> BoxFuture> { 73 | let event = event.clone(); 74 | let pool = self.pool.acquire(); 75 | async move { 76 | let p = pool.await.unwrap(); 77 | let _result = GiftCardRepository::create(&event, p).await; 78 | 79 | Ok(()) 80 | } 81 | .boxed() 82 | } 83 | } 84 | 85 | pub struct GiftCardRepository {} 86 | 87 | impl GiftCardRepository { 88 | // pub async fn find_all( 89 | // mut pool: sqlx::pool::PoolConnection, 90 | // ) -> Result, sqlx::Error> { 91 | // sqlx::query( 92 | // r#" 93 | // SELECT account_id, name, balance 94 | // FROM accounts 95 | // ORDER BY account_id 96 | // "#, 97 | // ) 98 | // .map(|row: PgRow| GiftCard { 99 | // account_id: Some(row.get(0)), 100 | // name: row.get(1), 101 | // status: GiftCardStatus::Initialized, 102 | // balance: row.get(2), 103 | // }) 104 | // .fetch_all(&mut pool) 105 | // .await 106 | // } 107 | 108 | pub async fn create( 109 | entity: &GiftCardCreated, 110 | mut pool: sqlx::pool::PoolConnection, 111 | ) -> Result { 112 | let mut tx = pool.begin().await?; 113 | let todo = sqlx::query( 114 | "INSERT INTO gift_cards (gift_card_id, name, price, count) VALUES ($1, $2, $3, $4) RETURNING gift_card_id, name, price, count", 115 | ) 116 | .bind(entity.gift_card_id) 117 | .bind(&entity.name) 118 | .bind(entity.price) 119 | .bind(entity.count) 120 | .map(|row: PgRow| GiftCard { 121 | gift_card_id: row.get(0), 122 | name: row.get(1), 123 | price: row.get::(2), 124 | count: row.get::(3), 125 | gift_card_state: GiftCardState::Created 126 | }) 127 | .fetch_one(&mut tx) 128 | .await?; 129 | 130 | tx.commit().await?; 131 | Ok(todo) 132 | } 133 | 134 | // pub async fn update( 135 | // account_id: &Uuid, 136 | // name: &str, 137 | // mut pool: sqlx::pool::PoolConnection, 138 | // ) -> Result { 139 | // let mut tx = pool.begin().await?; 140 | // let todo = sqlx::query( 141 | // "UPDATE accounts SET name = $1 WHERE account_id = $2 RETURNING account_id, name, balance", 142 | // ) 143 | // .bind(name) 144 | // .bind(account_id) 145 | // .map(|row: PgRow| GiftCard { 146 | // account_id: row.get(0), 147 | // name: row.get(1), 148 | // status: GiftCardStatus::Active, 149 | // balance: row.get(2), 150 | // }) 151 | // .fetch_one(&mut tx) 152 | // .await?; 153 | 154 | // tx.commit().await?; 155 | // Ok(todo) 156 | // } 157 | } 158 | -------------------------------------------------------------------------------- /examples/gift_shop/src/http.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use crate::account::*; 3 | use crate::commands::*; 4 | use actix_web::body::BoxBody; 5 | use actix_web::post; 6 | use actix_web::web; 7 | use actix_web::{HttpRequest, HttpResponse, Responder}; 8 | use serde_json::json; 9 | use uuid::Uuid; 10 | 11 | impl Responder for Account { 12 | type Body = BoxBody; 13 | fn respond_to(self, _req: &HttpRequest) -> HttpResponse { 14 | let body = serde_json::to_string(&self).unwrap(); 15 | 16 | HttpResponse::Ok() 17 | .content_type("application/json") 18 | .body(body) 19 | } 20 | } 21 | 22 | #[post("/accounts")] 23 | pub async fn create_account(account: web::Json) -> impl Responder { 24 | match Router::::dispatch::( 25 | account.into_inner().into(), 26 | CommandMetadatas::default(), 27 | ) 28 | .await 29 | { 30 | Ok(res) => HttpResponse::Ok().json(res.first()), 31 | Err(e) => HttpResponse::Ok().json(json!({ "error": e.to_string() })), 32 | } 33 | } 34 | 35 | #[post("/gift_cards")] 36 | pub async fn create_gift_card(gift_card: web::Json) -> impl Responder { 37 | match Router::::dispatch::( 38 | gift_card.into_inner().into(), 39 | CommandMetadatas::default(), 40 | ) 41 | .await 42 | { 43 | Ok(res) => HttpResponse::Ok().json(res.first()), 44 | Err(e) => HttpResponse::Ok().json(json!({ "error": e.to_string() })), 45 | } 46 | } 47 | 48 | #[post("/accounts/{account_id}/orders")] 49 | pub async fn create_order(params: web::Path) -> impl Responder { 50 | match Router::::dispatch::( 51 | CreateOrder { 52 | order_id: Uuid::new_v4(), 53 | account_id: params.into_inner(), 54 | }, 55 | CommandMetadatas::default(), 56 | ) 57 | .await 58 | { 59 | Ok(res) => HttpResponse::Ok().json(res.first()), 60 | Err(e) => HttpResponse::Ok().json(json!({ "error": e.to_string() })), 61 | } 62 | } 63 | 64 | #[post("/orders/{order_id}/items")] 65 | pub async fn add_order_item( 66 | params: web::Path, 67 | payload: web::Json, 68 | ) -> impl Responder { 69 | let payload = payload.into_inner(); 70 | match Router::::dispatch::( 71 | AddGiftCardToOrder { 72 | order_id: params.into_inner(), 73 | gift_card_id: payload.gift_card_id, 74 | amount: payload.amount, 75 | price: 10, 76 | }, 77 | CommandMetadatas::default(), 78 | ) 79 | .await 80 | { 81 | Ok(res) => HttpResponse::Ok().json(res.first()), 82 | Err(e) => HttpResponse::Ok().json(json!({ "error": e.to_string() })), 83 | } 84 | } 85 | 86 | #[derive(serde::Deserialize)] 87 | pub struct OpenAccountPayload { 88 | name: String, 89 | } 90 | 91 | #[derive(serde::Deserialize)] 92 | pub struct CreateGiftCardPayload { 93 | name: String, 94 | price: i64, 95 | count: usize, 96 | } 97 | 98 | #[derive(serde::Deserialize)] 99 | pub struct AddGiftCardToOrderPayload { 100 | gift_card_id: Uuid, 101 | amount: usize, 102 | } 103 | 104 | impl From for OpenAccount { 105 | fn from(payload: OpenAccountPayload) -> Self { 106 | Self { 107 | account_id: Uuid::new_v4(), 108 | name: payload.name, 109 | } 110 | } 111 | } 112 | 113 | impl From for CreateGiftCard { 114 | fn from(payload: CreateGiftCardPayload) -> Self { 115 | Self { 116 | gift_card_id: Uuid::new_v4(), 117 | name: payload.name, 118 | price: payload.price, 119 | count: payload.count, 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /examples/gift_shop/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | 3 | use actix_web::{ 4 | middleware, 5 | web::{self, Data}, 6 | App, HttpServer, 7 | }; 8 | use chekov::prelude::*; 9 | use sqlx::PgPool; 10 | 11 | mod account; 12 | mod commands; 13 | mod events; 14 | mod gift_card; 15 | mod http; 16 | mod order; 17 | 18 | pub fn init(cfg: &mut web::ServiceConfig) { 19 | cfg.service(http::create_account); 20 | cfg.service(http::create_gift_card); 21 | cfg.service(http::create_order); 22 | cfg.service(http::add_order_item); 23 | } 24 | 25 | #[derive(Default)] 26 | struct DefaultApp {} 27 | 28 | impl Application for DefaultApp { 29 | type Storage = PostgresStorage; 30 | } 31 | 32 | #[actix::main] 33 | async fn main() -> std::io::Result<()> { 34 | tracing_subscriber::fmt::init(); 35 | 36 | let db_pool = PgPool::connect("postgresql://postgres:postgres@localhost/gift_shop") 37 | .await 38 | .unwrap(); 39 | 40 | // Configure the storage (PG, InMemory,...) 41 | DefaultApp::with_default() 42 | .listener_url("postgresql://postgres:postgres@localhost/event_store_gift_shop".into()) 43 | .event_handler(account::AccountProjector { 44 | pool: db_pool.clone(), 45 | }) 46 | .event_handler(gift_card::GiftCardProjector { 47 | pool: db_pool.clone(), 48 | }) 49 | .event_handler(order::OrderProjector { 50 | pool: db_pool.clone(), 51 | }) 52 | .storage(PostgresStorage::with_url( 53 | "postgresql://postgres:postgres@localhost/event_store_gift_shop", 54 | )) 55 | .launch() 56 | .await; 57 | 58 | HttpServer::new(move || { 59 | App::new() 60 | .wrap(middleware::Logger::default()) 61 | .app_data(Data::new(db_pool.clone())) 62 | .app_data(web::JsonConfig::default().limit(4096)) 63 | .configure(init) 64 | }) 65 | .bind(env::var("RUNTIME_ENDPOINT").expect("Must define the RUNTIME_ENDPOINT"))? 66 | .run() 67 | .await?; 68 | 69 | tokio::signal::ctrl_c().await.unwrap(); 70 | println!("Ctrl-C received, shutting down"); 71 | Ok(()) 72 | } 73 | -------------------------------------------------------------------------------- /scripts/tests/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | 5 | postgres: 6 | image: postgres:13-beta1-alpine 7 | container_name: chekov-pg 8 | environment: 9 | # POSTGRES_DB: event_store 10 | POSTGRES_USER: postgres 11 | POSTGRES_PASSWORD: postgres 12 | # POSTGRES_HOST_AUTH_METHOD: scram-sha-256 13 | # POSTGRES_INITDB_ARGS: --auth-host=scram-sha-256 14 | volumes: 15 | - "./postgres/setup.sh:/docker-entrypoint-initdb.d/setup.sh" 16 | - "./postgres:/tmp" 17 | -------------------------------------------------------------------------------- /scripts/tests/postgres/setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | set -u 5 | 6 | function create_user_and_database() { 7 | local database=$1 8 | echo " Creating user and database '$database'" 9 | psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" <<-EOSQL 10 | CREATE USER $database; 11 | CREATE DATABASE $database; 12 | GRANT ALL PRIVILEGES ON DATABASE $database TO $database; 13 | EOSQL 14 | } 15 | 16 | create_user_and_database "event_store_bank" 17 | create_user_and_database "bank" 18 | create_user_and_database "event_store_gift_shop" 19 | create_user_and_database "gift_shop" 20 | 21 | psql -v ON_ERROR_STOP=1 --dbname=event_store_bank --username="event_store_bank" -f /tmp/setup_event_store.sql 22 | psql -v ON_ERROR_STOP=1 --dbname=bank --username="bank" -f /tmp/setup_bank.sql 23 | 24 | psql -v ON_ERROR_STOP=1 --dbname=event_store_gift_shop --username="event_store_gift_shop" -f /tmp/setup_event_store.sql 25 | psql -v ON_ERROR_STOP=1 --dbname=gift_shop --username="gift_shop" -f /tmp/setup_gift_shop.sql 26 | 27 | -------------------------------------------------------------------------------- /scripts/tests/postgres/setup_bank.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE "public"."accounts" ( 2 | "account_id" uuid NOT NULL, 3 | "name" varchar NOT NULL, 4 | PRIMARY KEY ("account_id") 5 | ); 6 | -------------------------------------------------------------------------------- /scripts/tests/postgres/setup_event_store.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE streams ( 2 | stream_id bigserial primary key, 3 | stream_uuid text NOT NULL, 4 | stream_version bigint DEFAULT 0 NOT NULL, 5 | created_at timestamp WITH time zone DEFAULT NOW( 6 | ) NOT NULL, 7 | deleted_at timestamp WITH time zone 8 | ); 9 | 10 | 11 | CREATE TABLE events ( 12 | event_id uuid PRIMARY KEY NOT NULL, 13 | event_type text NOT NULL, 14 | causation_id uuid NULL, 15 | correlation_id uuid NULL, 16 | data jsonb NOT NULL, 17 | metadata jsonb NULL, 18 | created_at timestamp WITH time zone DEFAULT NOW( 19 | ) NOT NULL 20 | ); 21 | 22 | CREATE TABLE stream_events ( 23 | event_id uuid NOT NULL REFERENCES events (event_id), 24 | stream_id bigserial NOT NULL REFERENCES streams (stream_id), 25 | stream_version bigint NOT NULL, 26 | original_stream_id bigserial REFERENCES streams (stream_id), 27 | original_stream_version bigint, 28 | PRIMARY KEY (event_id, stream_id) 29 | ); 30 | 31 | 32 | CREATE OR REPLACE FUNCTION notify_events() 33 | RETURNS trigger AS $$ 34 | DECLARE 35 | payload text; 36 | BEGIN 37 | -- Payload text contains: 38 | -- * `stream_uuid` 39 | -- * `stream_id` 40 | -- * first `stream_version` 41 | -- * last `stream_version` 42 | -- Each separated by a comma (e.g. 'stream-12345,1,1,5') 43 | payload := NEW.stream_uuid || ',' || NEW.stream_id || ',' || (OLD.stream_version + 1) || ',' || NEW.stream_version; 44 | -- Notify events to listeners 45 | PERFORM pg_notify('events', payload); 46 | RETURN NULL; 47 | END; 48 | $$ LANGUAGE plpgsql; 49 | 50 | CREATE OR REPLACE FUNCTION create_global_stream_events() 51 | RETURNS trigger AS $$ 52 | DECLARE 53 | all_stream streams%ROWTYPE; 54 | BEGIN 55 | UPDATE streams SET stream_version = stream_version + 1 WHERE stream_id = 0 RETURNING * INTO all_stream; 56 | INSERT INTO stream_events (event_id, stream_id, stream_version, original_stream_id, original_stream_version) VALUES (NEW.event_id, 0, all_stream.stream_version, NEW.stream_id, NEW.stream_version); 57 | 58 | RETURN NULL; 59 | END; 60 | $$ LANGUAGE plpgsql; 61 | 62 | CREATE TRIGGER event_notification AFTER UPDATE ON streams FOR EACH ROW EXECUTE PROCEDURE notify_events(); 63 | CREATE TRIGGER propagate_stream_events AFTER INSERT ON stream_events FOR EACH ROW WHEN (NEW.stream_id != 0 ) EXECUTE PROCEDURE create_global_stream_events(); 64 | 65 | /* CREATE UNIQUE INDEX ix_subscriptions_stream_uuid_subscription_name ON subscriptions (stream_uuid, subscription_name); */ 66 | CREATE UNIQUE INDEX ix_stream_events ON stream_events (stream_id, stream_version); 67 | CREATE UNIQUE INDEX ix_streams_stream_uuid ON streams (stream_uuid); 68 | 69 | CREATE RULE no_update_stream_events AS ON UPDATE TO stream_events DO INSTEAD NOTHING; 70 | CREATE RULE no_delete_stream_events AS ON DELETE TO stream_events DO INSTEAD NOTHING; 71 | CREATE RULE no_update_events AS ON UPDATE TO events DO INSTEAD NOTHING; 72 | CREATE RULE no_delete_events AS ON DELETE TO events DO INSTEAD NOTHING; 73 | 74 | INSERT INTO streams (stream_id, stream_uuid, stream_version) VALUES (0, '$all', 0); 75 | -------------------------------------------------------------------------------- /scripts/tests/postgres/setup_gift_shop.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE "public"."accounts" ( 2 | "account_id" uuid NOT NULL, 3 | "name" varchar NOT NULL, 4 | "balance" bigint NOT NULL DEFAULT 0, 5 | PRIMARY KEY ("account_id") 6 | ); 7 | 8 | CREATE TABLE "public"."gift_cards" ( 9 | "gift_card_id" uuid NOT NULL, 10 | "name" varchar NOT NULL, 11 | "price" bigint NOT NULL DEFAULT 0, 12 | "count" integer NOT NULL, 13 | PRIMARY KEY ("gift_card_id") 14 | ); 15 | 16 | CREATE TABLE "public"."orders" ( 17 | "order_id" uuid NOT NULL, 18 | "account_id" uuid NOT NULL, 19 | "items" jsonb NOT NULL DEFAULT '{}', 20 | "total_price" int8 NOT NULL DEFAULT 0, 21 | PRIMARY KEY ("order_id") 22 | ); 23 | 24 | ALTER TABLE "public"."orders" ADD FOREIGN KEY ("account_id") REFERENCES "public"."accounts" ("account_id"); 25 | -------------------------------------------------------------------------------- /scripts/tests/postgres/test.sql: -------------------------------------------------------------------------------- 1 | select current_user, current_database() 2 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | sonar.projectKey=Freyskeyd_chekov 2 | sonar.organization=freyskeyd 3 | 4 | # This is the name and version displayed in the SonarCloud UI. 5 | #sonar.projectName=chekov 6 | #sonar.projectVersion=1.0 7 | 8 | 9 | # Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows. 10 | #sonar.sources=. 11 | 12 | # Encoding of the source code. Default is default system encoding 13 | #sonar.sourceEncoding=UTF-8 14 | --------------------------------------------------------------------------------