diff --git a/birdie_snapshots/memory_store_load_events.accepted b/birdie_snapshots/memory_store_load_events.accepted new file mode 100644 index 0000000..90c7085 --- /dev/null +++ b/birdie_snapshots/memory_store_load_events.accepted @@ -0,0 +1,23 @@ +--- +version: 1.1.8 +title: memory store load events +file: ./test/eventsourcing_test.gleam +test_name: memory_store_load_events_test +--- +[ + MemoryStoreEventEnvelop( + "92085b42-032c-4d7a-84de-a86d67123858", + 1, + AccountOpened("92085b42-032c-4d7a-84de-a86d67123858"), + ), + MemoryStoreEventEnvelop( + "92085b42-032c-4d7a-84de-a86d67123858", + 2, + CustomerDepositedCash(10.0, 10.0), + ), + MemoryStoreEventEnvelop( + "92085b42-032c-4d7a-84de-a86d67123858", + 3, + CustomerWithdrewCash(5.99, 4.01), + ), +] \ No newline at end of file diff --git a/birdie_snapshots/sqlite_store_load_events.accepted b/birdie_snapshots/sqlite_store_load_events.accepted new file mode 100644 index 0000000..bf35c73 --- /dev/null +++ b/birdie_snapshots/sqlite_store_load_events.accepted @@ -0,0 +1,32 @@ +--- +version: 1.1.8 +title: sqlite store load events +file: ./test/sqlite_store_test.gleam +test_name: sqlite_store_load_events_test +--- +[ + SerializedEventEnvelop( + "92085b42-032c-4d7a-84de-a86d67123858", + 1, + AccountOpened("92085b42-032c-4d7a-84de-a86d67123858"), + "BankAccountEvent", + "1.0", + "BankAccount", + ), + SerializedEventEnvelop( + "92085b42-032c-4d7a-84de-a86d67123858", + 2, + CustomerDepositedCash(10.0, 10.0), + "BankAccountEvent", + "1.0", + "BankAccount", + ), + SerializedEventEnvelop( + "92085b42-032c-4d7a-84de-a86d67123858", + 3, + CustomerWithdrewCash(5.99, 4.01), + "BankAccountEvent", + "1.0", + "BankAccount", + ), +] \ No newline at end of file diff --git a/gleam.toml b/gleam.toml index 567c888..7e0dce2 100644 --- a/gleam.toml +++ b/gleam.toml @@ -1,5 +1,5 @@ name = "eventsourcing" -version = "0.2.0" +version = "1.0.0" # Fill out these fields if you intend to generate HTML documentation or publish # your project to the Hex package manager. diff --git a/src/eventsourcing/memory_store.gleam b/src/eventsourcing/memory_store.gleam index bf088e7..308465d 100644 --- a/src/eventsourcing/memory_store.gleam +++ b/src/eventsourcing/memory_store.gleam @@ -111,7 +111,7 @@ fn load_aggregate( let #(aggregate, sequence) = list.fold( - over: commited_events |> list.reverse, + over: commited_events, from: #(memory_store.empty_aggregate, 0), with: fn(aggregate_and_sequence, event_envelop) { let #(aggregate, _) = aggregate_and_sequence @@ -135,7 +135,7 @@ fn commit( let eventsourcing.AggregateContext(aggregate_id, _, sequence) = context let wrapped_events = wrap_events(aggregate_id, sequence, events) let past_events = load_commited_events(memory_store, aggregate_id) - let events = list.append(wrapped_events, past_events) + let events = list.append(past_events, wrapped_events) io.println( "storing: " <> wrapped_events |> list.length |> int.to_string diff --git a/test/eventsourcing_test.gleam b/test/eventsourcing_test.gleam index 3118762..a9d5736 100644 --- a/test/eventsourcing_test.gleam +++ b/test/eventsourcing_test.gleam @@ -1,6 +1,7 @@ import birdie import eventsourcing import eventsourcing/memory_store +import example_bank_account import gleam/int import gleam/io import gleam/list @@ -14,10 +15,16 @@ pub fn main() { pub fn memory_store_execute_test() { let mem_store = - memory_store.new(BankAccount(opened: False, balance: 0.0), handle, apply) + memory_store.new( + example_bank_account.BankAccount(opened: False, balance: 0.0), + example_bank_account.handle, + example_bank_account.apply, + ) let query = fn( aggregate_id: String, - events: List(eventsourcing.EventEnvelop(BankAccountEvent)), + events: List( + eventsourcing.EventEnvelop(example_bank_account.BankAccountEvent), + ), ) { io.println_error( "Aggregate Bank Account with ID: " @@ -31,7 +38,7 @@ pub fn memory_store_execute_test() { eventsourcing.execute( event_sourcing, "92085b42-032c-4d7a-84de-a86d67123858", - OpenAccount("92085b42-032c-4d7a-84de-a86d67123858"), + example_bank_account.OpenAccount("92085b42-032c-4d7a-84de-a86d67123858"), ) |> should.be_ok |> should.equal(Nil) @@ -39,7 +46,7 @@ pub fn memory_store_execute_test() { eventsourcing.execute( event_sourcing, "92085b42-032c-4d7a-84de-a86d67123858", - DepositMoney(10.0), + example_bank_account.DepositMoney(10.0), ) |> should.be_ok |> should.equal(Nil) @@ -47,7 +54,7 @@ pub fn memory_store_execute_test() { eventsourcing.execute( event_sourcing, "92085b42-032c-4d7a-84de-a86d67123858", - WithDrawMoney(5.99), + example_bank_account.WithDrawMoney(5.99), ) |> should.be_ok |> should.equal(Nil) @@ -60,49 +67,56 @@ pub fn memory_store_execute_test() { |> birdie.snap(title: "memory store") } -pub type BankAccount { - BankAccount(opened: Bool, balance: Float) -} - -pub type BankAccountCommand { - OpenAccount(account_id: String) - DepositMoney(amount: Float) - WithDrawMoney(amount: Float) -} +pub fn memory_store_load_events_test() { + let mem_store = + memory_store.new( + example_bank_account.BankAccount(opened: False, balance: 0.0), + example_bank_account.handle, + example_bank_account.apply, + ) + let query = fn( + aggregate_id: String, + events: List( + eventsourcing.EventEnvelop(example_bank_account.BankAccountEvent), + ), + ) { + io.println_error( + "Aggregate Bank Account with ID: " + <> aggregate_id + <> " commited " + <> events |> list.length |> int.to_string + <> " events.", + ) + } + let event_sourcing = eventsourcing.new(mem_store, [query]) + eventsourcing.execute( + event_sourcing, + "92085b42-032c-4d7a-84de-a86d67123858", + example_bank_account.OpenAccount("92085b42-032c-4d7a-84de-a86d67123858"), + ) + |> should.be_ok + |> should.equal(Nil) -pub type BankAccountEvent { - AccountOpened(account_id: String) - CustomerDepositedCash(amount: Float, balance: Float) - CustomerWithdrewCash(amount: Float, balance: Float) -} + eventsourcing.execute( + event_sourcing, + "92085b42-032c-4d7a-84de-a86d67123858", + example_bank_account.DepositMoney(10.0), + ) + |> should.be_ok + |> should.equal(Nil) -pub fn handle( - bank_account: BankAccount, - command: BankAccountCommand, -) -> Result(List(BankAccountEvent), Nil) { - case command { - OpenAccount(account_id) -> Ok([AccountOpened(account_id)]) - DepositMoney(amount) -> { - let balance = bank_account.balance +. amount - case amount >. 0.0 { - True -> Ok([CustomerDepositedCash(amount:, balance:)]) - False -> Error(Nil) - } - } - WithDrawMoney(amount) -> { - let balance = bank_account.balance -. amount - case amount >. 0.0 && balance >. 0.0 { - True -> Ok([CustomerWithdrewCash(amount:, balance:)]) - False -> Error(Nil) - } - } - } -} + eventsourcing.execute( + event_sourcing, + "92085b42-032c-4d7a-84de-a86d67123858", + example_bank_account.WithDrawMoney(5.99), + ) + |> should.be_ok + |> should.equal(Nil) -pub fn apply(bank_account: BankAccount, event: BankAccountEvent) { - case event { - AccountOpened(_) -> BankAccount(..bank_account, opened: True) - CustomerDepositedCash(_, balance) -> BankAccount(..bank_account, balance:) - CustomerWithdrewCash(_, balance) -> BankAccount(..bank_account, balance:) - } + memory_store.load_events( + mem_store.eventstore, + "92085b42-032c-4d7a-84de-a86d67123858", + ) + |> pprint.format + |> birdie.snap(title: "memory store load events") } diff --git a/test/example_bank_account.gleam b/test/example_bank_account.gleam new file mode 100644 index 0000000..f5669c1 --- /dev/null +++ b/test/example_bank_account.gleam @@ -0,0 +1,120 @@ +import decode +import gleam/dynamic +import gleam/json +import gleam/result + +pub type BankAccount { + BankAccount(opened: Bool, balance: Float) +} + +pub const bank_account_type = "BankAccount" + +pub type BankAccountCommand { + OpenAccount(account_id: String) + DepositMoney(amount: Float) + WithDrawMoney(amount: Float) +} + +pub type BankAccountEvent { + AccountOpened(account_id: String) + CustomerDepositedCash(amount: Float, balance: Float) + CustomerWithdrewCash(amount: Float, balance: Float) +} + +pub const bank_account_event_type = "BankAccountEvent" + +pub fn handle( + bank_account: BankAccount, + command: BankAccountCommand, +) -> Result(List(BankAccountEvent), Nil) { + case command { + OpenAccount(account_id) -> Ok([AccountOpened(account_id)]) + DepositMoney(amount) -> { + let balance = bank_account.balance +. amount + case amount >. 0.0 { + True -> Ok([CustomerDepositedCash(amount:, balance:)]) + False -> Error(Nil) + } + } + WithDrawMoney(amount) -> { + let balance = bank_account.balance -. amount + case amount >. 0.0 && balance >. 0.0 { + True -> Ok([CustomerWithdrewCash(amount:, balance:)]) + False -> Error(Nil) + } + } + } +} + +pub fn apply(bank_account: BankAccount, event: BankAccountEvent) { + case event { + AccountOpened(_) -> BankAccount(..bank_account, opened: True) + CustomerDepositedCash(_, balance) -> BankAccount(..bank_account, balance:) + CustomerWithdrewCash(_, balance) -> BankAccount(..bank_account, balance:) + } +} + +pub fn event_encoder(event: BankAccountEvent) -> String { + case event { + AccountOpened(account_id) -> + json.object([ + #("event-type", json.string("account-opened")), + #("account-id", json.string(account_id)), + ]) + CustomerDepositedCash(amount, balance) -> + json.object([ + #("event-type", json.string("customer-deposited-cash")), + #("amount", json.float(amount)), + #("balance", json.float(balance)), + ]) + CustomerWithdrewCash(amount, balance) -> + json.object([ + #("event-type", json.string("customer-withdrew-cash")), + #("amount", json.float(amount)), + #("balance", json.float(balance)), + ]) + } + |> json.to_string +} + +pub fn event_decoder( + string: String, +) -> Result(BankAccountEvent, List(dynamic.DecodeError)) { + let account_opened_decoder = + decode.into({ + use account_id <- decode.parameter + AccountOpened(account_id) + }) + |> decode.field("account-id", decode.string) + + let customer_deposited_cash = + decode.into({ + use amount <- decode.parameter + use balance <- decode.parameter + CustomerDepositedCash(amount, balance) + }) + |> decode.field("amount", decode.float) + |> decode.field("balance", decode.float) + + let customer_withdrew_cash = + decode.into({ + use amount <- decode.parameter + use balance <- decode.parameter + CustomerWithdrewCash(amount, balance) + }) + |> decode.field("amount", decode.float) + |> decode.field("balance", decode.float) + + let decoder = + decode.at(["event-type"], decode.string) + |> decode.then(fn(event_type) { + case event_type { + "account-opened" -> account_opened_decoder + "customer-deposited-cash" -> customer_deposited_cash + "customer-withdrew-cash" -> customer_withdrew_cash + _ -> decode.fail("event-type") + } + }) + json.decode(from: string, using: decode.from(decoder, _)) + |> result.map_error(fn(_) { [] }) +} diff --git a/test/sqlite_store_test.gleam b/test/sqlite_store_test.gleam index 32169cb..30e5a8a 100644 --- a/test/sqlite_store_test.gleam +++ b/test/sqlite_store_test.gleam @@ -1,13 +1,10 @@ import birdie -import decode import eventsourcing import eventsourcing/sqlite_store -import gleam/dynamic +import example_bank_account import gleam/int import gleam/io -import gleam/json import gleam/list -import gleam/result import gleeunit import gleeunit/should import pprint @@ -22,18 +19,23 @@ pub fn sqlite_store_test() { let sqlite_store = sqlite_store.new( sqlight_connection: db, - empty_entity: BankAccount(opened: False, balance: 0.0), - handle_command_function: handle, - apply_function: apply, - event_encoder: event_encoder, - event_decoder: event_decoder, - event_type: bank_account_event_type, + empty_entity: example_bank_account.BankAccount( + opened: False, + balance: 0.0, + ), + handle_command_function: example_bank_account.handle, + apply_function: example_bank_account.apply, + event_encoder: example_bank_account.event_encoder, + event_decoder: example_bank_account.event_decoder, + event_type: example_bank_account.bank_account_event_type, event_version: "1.0", - aggregate_type: bank_account_type, + aggregate_type: example_bank_account.bank_account_type, ) let query = fn( aggregate_id: String, - events: List(eventsourcing.EventEnvelop(BankAccountEvent)), + events: List( + eventsourcing.EventEnvelop(example_bank_account.BankAccountEvent), + ), ) { io.println_error( "Aggregate Bank Account with ID: " @@ -51,7 +53,7 @@ pub fn sqlite_store_test() { eventsourcing.execute( event_sourcing, "92085b42-032c-4d7a-84de-a86d67123858", - OpenAccount("92085b42-032c-4d7a-84de-a86d67123858"), + example_bank_account.OpenAccount("92085b42-032c-4d7a-84de-a86d67123858"), ) |> should.be_ok |> should.equal(Nil) @@ -59,7 +61,7 @@ pub fn sqlite_store_test() { eventsourcing.execute( event_sourcing, "92085b42-032c-4d7a-84de-a86d67123858", - DepositMoney(10.0), + example_bank_account.DepositMoney(10.0), ) |> should.be_ok |> should.equal(Nil) @@ -67,7 +69,7 @@ pub fn sqlite_store_test() { eventsourcing.execute( event_sourcing, "92085b42-032c-4d7a-84de-a86d67123858", - WithDrawMoney(5.99), + example_bank_account.WithDrawMoney(5.99), ) |> should.be_ok |> should.equal(Nil) @@ -80,118 +82,71 @@ pub fn sqlite_store_test() { |> birdie.snap(title: "sqlite store") } -pub type BankAccount { - BankAccount(opened: Bool, balance: Float) -} - -const bank_account_type = "BankAccount" - -pub type BankAccountCommand { - OpenAccount(account_id: String) - DepositMoney(amount: Float) - WithDrawMoney(amount: Float) -} - -pub type BankAccountEvent { - AccountOpened(account_id: String) - CustomerDepositedCash(amount: Float, balance: Float) - CustomerWithdrewCash(amount: Float, balance: Float) -} - -const bank_account_event_type = "BankAccountEvent" - -pub fn handle( - bank_account: BankAccount, - command: BankAccountCommand, -) -> Result(List(BankAccountEvent), Nil) { - case command { - OpenAccount(account_id) -> Ok([AccountOpened(account_id)]) - DepositMoney(amount) -> { - let balance = bank_account.balance +. amount - case amount >. 0.0 { - True -> Ok([CustomerDepositedCash(amount:, balance:)]) - False -> Error(Nil) - } - } - WithDrawMoney(amount) -> { - let balance = bank_account.balance -. amount - case amount >. 0.0 && balance >. 0.0 { - True -> Ok([CustomerWithdrewCash(amount:, balance:)]) - False -> Error(Nil) - } - } - } -} - -pub fn apply(bank_account: BankAccount, event: BankAccountEvent) { - case event { - AccountOpened(_) -> BankAccount(..bank_account, opened: True) - CustomerDepositedCash(_, balance) -> BankAccount(..bank_account, balance:) - CustomerWithdrewCash(_, balance) -> BankAccount(..bank_account, balance:) +pub fn sqlite_store_load_events_test() { + let assert Ok(db) = sqlight.open(":memory:") + let sqlite_store = + sqlite_store.new( + sqlight_connection: db, + empty_entity: example_bank_account.BankAccount( + opened: False, + balance: 0.0, + ), + handle_command_function: example_bank_account.handle, + apply_function: example_bank_account.apply, + event_encoder: example_bank_account.event_encoder, + event_decoder: example_bank_account.event_decoder, + event_type: example_bank_account.bank_account_event_type, + event_version: "1.0", + aggregate_type: example_bank_account.bank_account_type, + ) + let query = fn( + aggregate_id: String, + events: List( + eventsourcing.EventEnvelop(example_bank_account.BankAccountEvent), + ), + ) { + io.println_error( + "Aggregate Bank Account with ID: " + <> aggregate_id + <> " commited " + <> events |> list.length |> int.to_string + <> " events.", + ) } -} + sqlite_store.create_event_table(sqlite_store.eventstore) + |> should.be_ok -pub fn event_encoder(event: BankAccountEvent) -> String { - case event { - AccountOpened(account_id) -> - json.object([ - #("event-type", json.string("account-opened")), - #("account-id", json.string(account_id)), - ]) - CustomerDepositedCash(amount, balance) -> - json.object([ - #("event-type", json.string("customer-deposited-cash")), - #("amount", json.float(amount)), - #("balance", json.float(balance)), - ]) - CustomerWithdrewCash(amount, balance) -> - json.object([ - #("event-type", json.string("customer-withdrew-cash")), - #("amount", json.float(amount)), - #("balance", json.float(balance)), - ]) - } - |> json.to_string -} + let event_sourcing = eventsourcing.new(sqlite_store, [query]) -pub fn event_decoder( - string: String, -) -> Result(BankAccountEvent, List(dynamic.DecodeError)) { - let account_opened_decoder = - decode.into({ - use account_id <- decode.parameter - AccountOpened(account_id) - }) - |> decode.field("account-id", decode.string) + eventsourcing.execute( + event_sourcing, + "92085b42-032c-4d7a-84de-a86d67123858", + example_bank_account.OpenAccount("92085b42-032c-4d7a-84de-a86d67123858"), + ) + |> should.be_ok + |> should.equal(Nil) - let customer_deposited_cash = - decode.into({ - use amount <- decode.parameter - use balance <- decode.parameter - CustomerDepositedCash(amount, balance) - }) - |> decode.field("amount", decode.float) - |> decode.field("balance", decode.float) + eventsourcing.execute( + event_sourcing, + "92085b42-032c-4d7a-84de-a86d67123858", + example_bank_account.DepositMoney(10.0), + ) + |> should.be_ok + |> should.equal(Nil) - let customer_withdrew_cash = - decode.into({ - use amount <- decode.parameter - use balance <- decode.parameter - CustomerWithdrewCash(amount, balance) - }) - |> decode.field("amount", decode.float) - |> decode.field("balance", decode.float) + eventsourcing.execute( + event_sourcing, + "92085b42-032c-4d7a-84de-a86d67123858", + example_bank_account.WithDrawMoney(5.99), + ) + |> should.be_ok + |> should.equal(Nil) - let decoder = - decode.at(["event-type"], decode.string) - |> decode.then(fn(event_type) { - case event_type { - "account-opened" -> account_opened_decoder - "customer-deposited-cash" -> customer_deposited_cash - "customer-withdrew-cash" -> customer_withdrew_cash - _ -> decode.fail("event-type") - } - }) - json.decode(from: string, using: decode.from(decoder, _)) - |> result.map_error(fn(_) { [] }) + sqlite_store.load_events( + sqlite_store.eventstore, + "92085b42-032c-4d7a-84de-a86d67123858", + ) + |> should.be_ok + |> pprint.format + |> birdie.snap(title: "sqlite store load events") }