### Installation Source: https://github.com/hnaderi/edomata/blob/main/docs/backends/doobie.md Instructions on how to add Edomata Doobie dependencies to your project. ```APIDOC ## Installation ### Using Doobie ```scala libraryDependencies += "dev.hnaderi" %% "edomata-doobie" % "@VERSION@" ``` ### Using Doobie with Circe ```scala libraryDependencies += "dev.hnaderi" %% "edomata-doobie-circe" % "@VERSION@" ``` ### Using Doobie with uPickle ```scala libraryDependencies += "dev.hnaderi" %% "edomata-doobie-upickle" % "@VERSION@" ``` **Note:** These packages are JVM-only due to their dependency on JDBC. ``` -------------------------------- ### Install Edomata Dependencies Source: https://context7.com/hnaderi/edomata/llms.txt Configuration for adding Edomata modules to a Scala project via sbt. Includes core libraries for JVM, Scala.js, and Scala Native, as well as database backends. ```scala libraryDependencies += "dev.hnaderi" %% "edomata-core" % "0.12.0" libraryDependencies += "dev.hnaderi" %%% "edomata-core" % "0.12.0" libraryDependencies += "dev.hnaderi" %% "edomata-skunk-circe" % "0.12.0" libraryDependencies += "dev.hnaderi" %% "edomata-doobie-circe" % "0.12.0" ``` -------------------------------- ### Scala Domain Model Testing Source: https://github.com/hnaderi/edomata/blob/main/docs/tutorials/1-2_cqrs.md Provides examples of testing the pure domain logic of the Order model in Scala, demonstrating how to invoke methods like place and markAsCooking and verify their outcomes. ```scala Order.Empty.place("kebab", "home") Order.Placed("pizza", "office").markAsCooking("chef") ``` -------------------------------- ### Test Scala Domain Model States and Transitions Source: https://github.com/hnaderi/edomata/blob/main/docs/tutorials/1-1_eventsourcing.md Provides examples of testing the defined domain model states and transitions in Scala. It shows how to create initial states, apply events to transition between states, and handle potential outcomes like opening, depositing, closing, and chaining operations. ```scala Account.New.open Account.Open(10).deposit(2) Account.Open(5).close Account.New.open.flatMap(_.close) ``` -------------------------------- ### Install Edomata Skunk Dependencies Source: https://github.com/hnaderi/edomata/blob/main/docs/backends/skunk.md Add the necessary library dependencies to your build.sbt file. This includes the core Skunk module, optional JSON integration modules, and Scala.js support. ```scala libraryDependencies += "dev.hnaderi" %% "edomata-skunk" % "@VERSION@" libraryDependencies += "dev.hnaderi" %% "edomata-skunk-circe" % "@VERSION@" libraryDependencies += "dev.hnaderi" %% "edomata-skunk-upickle" % "@VERSION@" libraryDependencies += "dev.hnaderi" %%% "edomata-skunk" % "@VERSION@" ``` -------------------------------- ### Scala EitherNec Examples Source: https://github.com/hnaderi/edomata/blob/main/docs/tutorials/1-2_cqrs.md Demonstrates the usage and composability of EitherNec with Right and Left values, including mapping and chaining operations using standard Either and for-comprehension. ```scala val e1 = Right(1) val e2 = "Missile Launched!".asRight val e3 = "No remained missiles to launch!".leftNec val e4 = e1.map(_ * 2) val e5 = e2 >> e1 val e6 = for { a <- e4 b <- e5 } yield a + b ``` -------------------------------- ### Define Backend Codecs with Doobie and Circe Source: https://github.com/hnaderi/edomata/blob/main/docs/backends/doobie.md Configure codecs for your event and state models when using Doobie with edomata. This example uses Circe for JSON serialization, supporting both event sourcing and CQRS styles. Use `.jsonb` for binary JSON or `.json` for text JSON. ```scala given BackendCodec[Event] = CirceCodec.jsonb // or .json given BackendCodec[Notification] = CirceCodec.jsonb ``` ```scala given BackendCodec[State] = CirceCodec.jsonb ``` ```scala given BackendCodec[State] = CirceCodec.jsonb ``` -------------------------------- ### Install Doobie Dependencies for edomata Source: https://github.com/hnaderi/edomata/blob/main/docs/backends/doobie.md Add the necessary Doobie dependencies to your project's build file. Choose the appropriate package based on whether you need integrated modules like Circe or uPickle. Note that these packages are JVM-only. ```scala libraryDependencies += "dev.hnaderi" %% "edomata-doobie" % "@VERSION@" ``` ```scala libraryDependencies += "dev.hnaderi" %% "edomata-doobie-circe" % "@VERSION@" libraryDependencies += "dev.hnaderi" %% "edomata-doobie-upickle" % "@VERSION@" ``` -------------------------------- ### Compile Application Backend with Skunk Source: https://github.com/hnaderi/edomata/blob/main/docs/backends/skunk.md Build a backend service using the SkunkDriver or SkunkCQRSDriver. This snippet demonstrates initializing the backend with a session pool, snapshot configuration, and retry logic. ```scala val app = ??? val pool : Resource[IO, Session[IO]] = ??? val buildBackend = Backend .builder(AccountService) .use(SkunkDriver("domainname", pool)) .inMemSnapshot(200) .withRetryConfig(retryInitialDelay = 2.seconds) .build val application = buildBackend.use { backend => val service = backend.compile(app) service(CommandMessage("abc", Instant.now, "a", "receive")).flatMap(IO.println) } ``` -------------------------------- ### Execute and Run Edomatons Source: https://github.com/hnaderi/edomata/blob/main/docs/tutorials/1-1_eventsourcing.md Demonstrates how to execute an Edomaton using different monads like Id or IO, and how to retrieve raw responses. ```scala import cats.Id val obtained = AccountService[Id].execute(scenario1) import cats.effect.IO AccountService[IO].execute(scenario1) AccountService[Id].run(scenario1) ``` -------------------------------- ### Initialize and Create Decisions in Edomata Source: https://github.com/hnaderi/edomata/blob/main/docs/tutorials/1-1_eventsourcing.md Demonstrates how to create Decision instances using constructors or extension methods. It covers basic acceptance, rejection, and pure value wrapping. ```scala import edomata.syntax.all.* val d1 = Decision(1) val d2 = Decision.accept("Missile Launched!") val d3 = Decision.reject("No remained missiles to launch!") 1.asDecision "Missile Launched!".accept "No remained missiles to launch!".reject ``` -------------------------------- ### Create RequestContext for Testing Source: https://github.com/hnaderi/edomata/blob/main/docs/tutorials/1-1_eventsourcing.md Sets up a RequestContext to simulate an incoming command and current state for testing an Edomaton. ```scala import java.time.Instant val scenario1 = RequestContext( command = CommandMessage( id = "some random id for request", time = Instant.MIN, address = "our account id", payload = Command.Open ), state = Account.New ) ``` -------------------------------- ### State-Based Model Definition with CQRSModel in Scala Source: https://context7.com/hnaderi/edomata/llms.txt Defines aggregates for CQRS-style applications without event sourcing using CQRSModel. The state is stored directly without an event history. This example demonstrates a food delivery domain with various order statuses and state transitions. ```scala import edomata.core.* import cats.implicits.* import cats.data.EitherNec // Define domain for food delivery enum OrderStatus { case New case Cooking(cook: String) case WaitingToPickUp case Delivering(unit: String) } enum Rejection { case ExistingOrder case NoSuchOrder case InvalidRequest } enum Order { case Empty case Placed(food: String, address: String, status: OrderStatus = OrderStatus.New) case Delivered(rating: Int) def place(food: String, address: String): EitherNec[Rejection, Order] = this match { case Order.Empty => Order.Placed(food, address).asRight case _ => Rejection.ExistingOrder.leftNec } def markAsCooking(cook: String): EitherNec[Rejection, Order] = this match { case st @ Order.Placed(_, _, OrderStatus.New) => st.copy(status = OrderStatus.Cooking(cook)).asRight case _ => Rejection.InvalidRequest.leftNec } def markAsCooked: EitherNec[Rejection, Order] = this match { case st @ Order.Placed(_, _, OrderStatus.Cooking(_)) => st.copy(status = OrderStatus.WaitingToPickUp).asRight case _ => Rejection.InvalidRequest.leftNec } def markAsDelivered(score: Int): EitherNec[Rejection, Order] = this match { case Order.Placed(_, _, OrderStatus.Delivering(_)) => Order.Delivered(score).asRight case _ => Rejection.InvalidRequest.leftNec } } // Define CQRS model object Order extends CQRSModel[Order, Rejection] { def initial: Order = Order.Empty } // Test the model Order.Empty.place("pizza", "home") // Right(Placed(pizza, home, New)) Order.Placed("pizza", "home").markAsCooking("chef") // Right(Placed(..., Cooking(chef))) ``` -------------------------------- ### Implement AccountService Source: https://github.com/hnaderi/edomata/blob/main/docs/tutorials/1-1_eventsourcing.md Implements a service using the Edomata router to handle commands and publish notifications. It utilizes a tagless final style for flexibility. ```scala object AccountService extends Account.Service[Command, Notification] { import cats.Monad def apply[F[_] : Monad] : App[F, Unit] = App.router { case Command.Open => for { ns <- App.state.decide(_.open) acc <- App.aggregateId _ <- App.publish(Notification.AccountOpened(acc)) } yield () case Command.Deposit(amount) => for { deposited <- App.state.decide(_.deposit(amount)) accId <- App.aggregateId _ <- App.publish(Notification.BalanceUpdated(accId, deposited.balance)) } yield () case Command.Withdraw(amount) => for { withdrawn <- App.state.decide(_.withdraw(amount)) accId <- App.aggregateId _ <- App.publish(Notification.BalanceUpdated(accId, withdrawn.balance)) } yield () case Command.Close => App.state.decide(_.close).void } } ``` -------------------------------- ### Define Scala Domain Model with Edomata Source: https://github.com/hnaderi/edomata/blob/main/docs/tutorials/1-1_eventsourcing.md Demonstrates how to define a domain model for an Account using Edomata's DomainModel. It specifies the initial state and the transition logic for various events like Opened, Withdrawn, Deposited, and Closed. This approach ensures model consistency and handles potential conflicts. ```scala object Account extends DomainModel[Account, Event, Rejection] { def initial = New // 1 def transition = { // 2 case Event.Opened => _ => Open(0).validNec case Event.Withdrawn(b) => _.mustBeOpen.map(s => s.copy(balance = s.balance - b)) // 3 case Event.Deposited(b) => _.mustBeOpen.map(s => s.copy(balance = s.balance + b)) case Event.Closed => _=> Close.validNec } } ``` -------------------------------- ### Reading Event Journals and Aggregate States with Edomata Backend Source: https://context7.com/hnaderi/edomata/llms.txt Demonstrates how to access event journals and aggregate repositories using the Edomata backend. This includes reading all events, events after a specific sequence number, individual aggregate streams, and the current state or full history of an aggregate. ```scala import edomata.backend.* import edomata.backend.eventsourcing.* import cats.effect.IO import fs2.Stream // Assuming you have a backend instance def backend: Backend[IO, Account, Event, Rejection, Notification] = ??? // Read entire journal from beginning val allEvents: Stream[IO, JournalEntry[Event]] = backend.journal.readAll // Read events after a specific sequence number (for projections) val eventsAfter100: Stream[IO, JournalEntry[Event]] = backend.journal.readAllAfter(100) // Read single aggregate stream val accountEvents: Stream[IO, JournalEntry[Event]] = backend.journal.readStream("account-001") // Read events before a specific sequence (for replay) val eventsBefore: Stream[IO, JournalEntry[Event]] = backend.journal.readStreamBefore("account-001", 50) // Read current state of an aggregate val currentState: IO[AggregateState[Account, Event, Rejection]] = backend.repository.get("account-001") // Read full history with state at each point val history: Stream[IO, AggregateState[Account, Event, Rejection]] = backend.repository.history("account-001") ``` -------------------------------- ### Compile Application to Service with DoobieDriver Source: https://github.com/hnaderi/edomata/blob/main/docs/backends/doobie.md Build your edomata application into a runnable service using Doobie drivers. This involves creating a Transactor, configuring the Backend builder with the appropriate Doobie driver (DoobieDriver for event sourcing or DoobieCQRSDriver for CQRS), and optionally configuring snapshotting and retry policies. The compiled service can then process commands. ```scala val app = ??? // your application from previous chapter val trx : Transactor[IO] = ??? // create your Transactor val buildBackend = Backend .builder(AccountService) // 1 .use(DoobieDriver("domainname", trx)) // 2 // .persistedSnapshot(maxInMem = 200) // 3 .inMemSnapshot(200) .withRetryConfig(retryInitialDelay = 2.seconds) .build val application = buildBackend.use { backend => val service = backend.compile(app) // compiling your application will give you a function // that takes a messages and does everything required, // and returns result. service( CommandMessage("abc", Instant.now, "a", "receive") ).flatMap(IO.println) ``` -------------------------------- ### Edomaton Service Layer Implementation in Scala Source: https://context7.com/hnaderi/edomata/llms.txt Demonstrates building an event-driven service layer using Edomaton. It defines commands, notifications, and a service that handles state transitions, side effects, and publishes notifications. Supports both pure functional testing with cats.Id and real-world applications with cats.effect.IO. ```scala import edomata.core.* import edomata.syntax.all.* import cats.Monad import cats.effect.IO import java.time.Instant // Define commands and notifications enum Command { case Open case Deposit(amount: BigDecimal) case Withdraw(amount: BigDecimal) case Close } enum Notification { case AccountOpened(accountId: String) case BalanceUpdated(accountId: String, balance: BigDecimal) case AccountClosed(accountId: String) } // Create service using the DSL object AccountService extends Account.Service[Command, Notification] { def apply[F[_]: Monad]: App[F, Unit] = App.router { case Command.Open => for { ns <- App.state.decide(_.open) acc <- App.aggregateId _ <- App.publish(Notification.AccountOpened(acc)) } yield () case Command.Deposit(amount) => for { deposited <- App.state.decide(_.deposit(amount)) accId <- App.aggregateId _ <- App.publish(Notification.BalanceUpdated(accId, deposited.balance)) } yield () case Command.Withdraw(amount) => for { withdrawn <- App.state.decide(_.withdraw(amount)) accId <- App.aggregateId _ <- App.publish(Notification.BalanceUpdated(accId, withdrawn.balance)) } yield () case Command.Close => App.state.decide(_.close).void } } // Testing the Edomaton import cats.Id val testScenario = RequestContext( command = CommandMessage( id = "cmd-123", time = Instant.now, address = "account-001", payload = Command.Open ), state = Account.New ) val result = AccountService[Id].execute(testScenario) // EdomatonResult.Accepted( // newState = Account.Open(0), // events = Chain(Event.Opened), // notifications = Chain(Notification.AccountOpened("account-001")) // ) // Using with IO for real applications val ioResult: IO[EdomatonResult[Account, Event, Rejection, Notification, Unit]] = AccountService[IO].execute(testScenario) ``` -------------------------------- ### Compiling Application to a Service Source: https://github.com/hnaderi/edomata/blob/main/docs/backends/doobie.md Steps to compile your Edomata application into a runnable service using Doobie drivers. ```APIDOC ## Compiling Application to a Service To build your backend, you need to choose a Doobie driver: 1. `DoobieDriver`: For event sourcing. 2. `DoobieCQRSDriver`: For CQRS style. ### Example Compilation ```scala val app = ??? // Your application defined previously val trx : Transactor[IO] = ??? // Your configured Transactor[IO] val buildBackend = Backend .builder(AccountService) // 1. Provide your domain service .use(DoobieDriver("domainname", trx)) // 2. Use the Doobie driver (schema name is "domainname") // .persistedSnapshot(maxInMem = 200) // 3. Optional: Configure persisted snapshots .inMemSnapshot(200) // Configure in-memory snapshots .withRetryConfig(retryInitialDelay = 2.seconds) // Configure retry behavior .build val application = buildBackend.use { backend => val service = backend.compile(app) // The compiled service takes messages and processes them. service( CommandMessage("abc", Instant.now, "a", "receive") ).flatMap(IO.println) } ``` **Notes:** 1. `AccountService` represents your domain service. 2. `"domainname"` is used as the schema name in PostgreSQL. 3. Explore the `Backend.builder` options for further customization. ``` -------------------------------- ### Implementing Outbox Consumer for Integration Events in Edomata Source: https://context7.com/hnaderi/edomata/llms.txt Shows how to use the OutboxConsumer to reliably publish integration events to external systems. The consumer processes notifications from the backend and allows custom logic for publishing based on the event type. ```scala import edomata.backend.* import edomata.backend.eventsourcing.* import cats.effect.IO import fs2.Stream def backend: Backend[IO, Account, Event, Rejection, Notification] = ??? // Create an outbox consumer that publishes to external systems val publisher: Stream[IO, Nothing] = OutboxConsumer(backend) { item => // item contains: id, aggregateId, seqNr, notification, metadata item.notification match { case Notification.AccountOpened(accountId) => // Publish to message broker (Kafka, RabbitMQ, etc.) IO.println(s"Publishing AccountOpened for $accountId") case Notification.BalanceUpdated(accountId, balance) => // Send webhook or update external system IO.println(s"Balance updated: $accountId -> $balance") case Notification.AccountClosed(accountId) => IO.println(s"Account closed: $accountId") } } // Run the publisher as a background process val runPublisher: IO[Unit] = publisher.compile.drain ``` -------------------------------- ### Skunk Backend Configuration for Edomata in Scala Source: https://context7.com/hnaderi/edomata/llms.txt Configures the Skunk backend for event sourcing or CQRS persistence with PostgreSQL. Supports JVM, JS, and Native platforms. Requires Skunk PostgreSQL library and Cats Effect. Defines JSON codecs for domain types and configures session pools and retry strategies. ```scala import edomata.backend.Backend import edomata.skunk.* import cats.effect.{IO, Resource} import skunk.Session import scala.concurrent.duration.* // Define JSON codecs for your domain types given BackendCodec[Event] = CirceCodec.jsonb given BackendCodec[Notification] = CirceCodec.jsonb // For persisted snapshots (optional): // given BackendCodec[Account] = CirceCodec.jsonb // Create Skunk session pool val sessionPool: Resource[IO, Session[IO]] = Session.pooled[IO]( host = "localhost", port = 5432, user = "postgres", database = "postgres", password = Some("postgres"), max = 10 ) // Build event sourcing backend val backendResource = Backend .builder(AccountService) .use(SkunkDriver("accounts", sessionPool)) // Schema name: "accounts" .inMemSnapshot(200) // In-memory snapshot cache size // .persistedSnapshot(maxInMem = 200) // Or use persisted snapshots .withRetryConfig(retryInitialDelay = 2.seconds) .build // Run the application val application: IO[Unit] = backendResource.use { backend => val service = backend.compile(AccountService[IO]) for { result <- service(CommandMessage("cmd-1", java.time.Instant.now, "acc-001", Command.Open)) _ <- IO.println(s"Result: $result") } yield () } // For CQRS (non-event-sourced) applications val cqrsBackend = Backend .builder(OrderService) .use(SkunkCQRSDriver("orders", sessionPool)) .withRetryConfig(retryInitialDelay = 2.seconds) .build ``` -------------------------------- ### Implement Decision Logic Source: https://context7.com/hnaderi/edomata/llms.txt Demonstrates the usage of the Decision monad for handling event-driven logic. Shows how to create, compose, and transform decisions using native syntax and Cats integration. ```scala import edomata.core.* import edomata.syntax.all.* import cats.implicits.* val pureValue = Decision(1) val acceptEvent = Decision.accept("AccountOpened") val rejectWithError = Decision.reject("InsufficientFunds") val pureDecision = 1.asDecision val eventAccepted = "AccountOpened".accept val rejected = "InsufficientFunds".reject val composed = for { i <- Decision.pure(1) _ <- Decision.accept("EventA") _ <- Decision.accept("EventB", "EventC") j <- Decision.acceptReturn(i * 2)("EventD", "EventE") } yield i + j val combined = (Decision(1), Decision(2)).mapN(_ + _) val traversed = List.range(1, 5).traverse(Decision.accept(_)) val decision = Decision.accept("Event").as(42) decision.toValidated decision.toEither decision.toOption ``` -------------------------------- ### Imports Source: https://github.com/hnaderi/edomata/blob/main/docs/backends/doobie.md Essential imports for using Doobie with Edomata. ```APIDOC ## Imports ```scala import edomata.doobie.* ``` ``` -------------------------------- ### Add Edomata Core Dependency (Scala.js/Scala Native) Source: https://github.com/hnaderi/edomata/blob/main/README.md This snippet illustrates how to add the Edomata core library for projects targeting Scala.js or Scala Native. The `%%%` syntax is used to specify cross-platform compatibility for these environments. ```scala libraryDependencies += "dev.hnaderi" %%% "edomata-core" % "" ``` -------------------------------- ### Using CommandMessage and RequestContext in Edomata Source: https://context7.com/hnaderi/edomata/llms.txt Explains the `CommandMessage` structure for incoming commands, including metadata for tracking and correlation. It also details how `RequestContext` bundles commands with the current aggregate state, and how to access these within a service. ```scala import edomata.core.* import java.time.Instant // Create a root command (first in a chain) val rootCommand = CommandMessage( id = "cmd-001", time = Instant.now, address = "account-123", // Aggregate ID payload = Command.Open ) // Create command with explicit metadata (for chained commands) val chainedCommand = CommandMessage( id = "cmd-002", time = Instant.now, address = "account-123", payload = Command.Deposit(100), metadata = MessageMetadata( correlation = Some("cmd-001"), // Original root command causation = Some("cmd-001") // Direct cause ) ) // Derive metadata for next message in chain val derivedMeta: MessageMetadata = rootCommand.deriveMeta // MessageMetadata(correlation = Some("cmd-001"), causation = Some("cmd-001")) // Build request context from command val context: RequestContext[Command, Account] = rootCommand.buildContext(Account.New) // Access context fields in your service object MyService extends Account.Service[Command, Notification] { def apply[F[_]: cats.Monad]: App[F, Unit] = for { ctx <- App.context // Full RequestContext cmd <- App.command // CommandMessage state <- App.state // Current aggregate state aggId <- App.aggregateId // Aggregate address/ID cmdId <- App.messageId // Command ID meta <- App.metadata // Message metadata } yield () } ``` -------------------------------- ### Doobie Backend Configuration for Edomata in Scala Source: https://context7.com/hnaderi/edomata/llms.txt Configures the Doobie backend for event sourcing or CQRS persistence with PostgreSQL using JDBC. This configuration is JVM-only. It requires the Doobie library and Cats Effect. It covers creating a transactor, defining JSON codecs, and building the backend resource with retry configurations. ```scala import edomata.backend.Backend import edomata.doobie.* import cats.effect.IO import doobie.util.transactor.Transactor import scala.concurrent.duration.* // Define JSON codecs given BackendCodec[Event] = CirceCodec.jsonb given BackendCodec[Notification] = CirceCodec.jsonb // Create Doobie transactor val transactor: Transactor[IO] = Transactor.fromDriverManager[IO]( driver = "org.postgresql.Driver", url = "jdbc:postgresql://localhost:5432/postgres", user = "postgres", password = "postgres", logHandler = None ) // Build event sourcing backend val backendResource = Backend .builder(AccountService) .use(DoobieDriver("accounts", transactor)) .inMemSnapshot(200) .withRetryConfig(retryInitialDelay = 2.seconds) .build // Run the application val application: IO[Unit] = backendResource.use { backend => val service = backend.compile(AccountService[IO]) service(CommandMessage("cmd-1", java.time.Instant.now, "acc-001", Command.Open)) .flatMap(IO.println) } // For CQRS applications val cqrsBackend = Backend .builder(OrderService) .use(DoobieCQRSDriver("orders", transactor)) .withRetryConfig(retryInitialDelay = 2.seconds) .build ``` -------------------------------- ### Define Edomata Commands and Notifications Source: https://github.com/hnaderi/edomata/blob/main/docs/tutorials/1-1_eventsourcing.md Defines the core ADTs for the business domain. Commands represent user intentions, while Notifications represent the resulting events. ```scala enum Command { case Open case Deposit(amount: BigDecimal) case Withdraw(amount: BigDecimal) case Close } enum Notification { case AccountOpened(accountId: String) case BalanceUpdated(accountId: String, balance: BigDecimal) case AccountClosed(accountId: String) } ``` -------------------------------- ### Add Edomata Core Dependency (Scala) Source: https://github.com/hnaderi/edomata/blob/main/README.md This snippet shows how to add the core Edomata library to your Scala project's build dependencies. It uses the Scala Build Tool (sbt) syntax and specifies the artifact and version. The `%%` indicates that the Scala version will be automatically appended. ```scala libraryDependencies += "dev.hnaderi" %% "edomata-core" % "" ``` -------------------------------- ### Define Backend Codecs Source: https://github.com/hnaderi/edomata/blob/main/docs/backends/skunk.md Configure BackendCodec instances for your event and state models using Circe. This allows the system to serialize and deserialize data for Postgres storage. ```scala import edomata.skunk.* given BackendCodec[Event] = CirceCodec.jsonb given BackendCodec[Notification] = CirceCodec.jsonb given BackendCodec[State] = CirceCodec.jsonb ``` -------------------------------- ### Add Edomata Skunk Circe Dependency (Scala) Source: https://github.com/hnaderi/edomata/blob/main/README.md This snippet demonstrates how to include the Edomata Skunk Circe module, which likely provides integration with Skunk (a PostgreSQL client) and Circe (a JSON library) for Scala projects. The `%%` indicates Scala version compatibility. ```scala libraryDependencies += "dev.hnaderi" %% "edomata-skunk-circe" % "" ``` -------------------------------- ### Codec Definitions Source: https://github.com/hnaderi/edomata/blob/main/docs/backends/doobie.md How to define codecs for events, notifications, and states when using Doobie. ```APIDOC ## Defining Codecs ### For Event Sourcing When using event sourcing, define codecs for `Event` and `Notification`. ```scala given BackendCodec[Event] = CirceCodec.jsonb // or .json given BackendCodec[Notification] = CirceCodec.jsonb ``` ### For CQRS Style When using a CQRS style, define the codec for `State`. ```scala given BackendCodec[State] = CirceCodec.jsonb ``` ### For Persisted Snapshots (Event Sourcing Only) If you intend to use persisted snapshots, you must also provide a codec for your state model. ```scala given BackendCodec[State] = CirceCodec.jsonb ``` ``` -------------------------------- ### Import Doobie Backend for edomata Source: https://github.com/hnaderi/edomata/blob/main/docs/backends/doobie.md Import the Doobie backend functionalities into your Scala project to enable database operations. ```scala import edomata.doobie.* ``` -------------------------------- ### Consume Outbox Items with OutboxConsumer Source: https://github.com/hnaderi/edomata/blob/main/docs/tutorials/3_processes.md Demonstrates how to use the OutboxConsumer to process outboxed items atomically. It marks items as read after the provided action executes. ```scala def publisher : Stream[IO, Nothing] = OutboxConsumer(backend){ item => // use outboxed item ??? } ``` -------------------------------- ### Test Stomaton with Id Effect Source: https://github.com/hnaderi/edomata/blob/main/docs/tutorials/1-2_cqrs.md Demonstrates how to test a Stomaton by running it with the cats.Id effect. It executes a command against an initial state and allows for assertion on the result and notifications. ```scala import java.time.Instant val srv = OrderService[cats.Id] val scenario1 = srv.run( CommandMessage( id = "cmd id", time = Instant.MIN, address = "aggregate id", payload = Command.Place("taco", "home") ), Order.Empty ) scenario1.result scenario1.notifications ``` -------------------------------- ### CommandMessage and Context Source: https://context7.com/hnaderi/edomata/llms.txt Handling incoming commands with metadata for correlation and causation tracking. ```APIDOC ## POST /command/execute ### Description Submits a command to the system with associated metadata for tracking. ### Method POST ### Endpoint /command/execute ### Request Body - **id** (String) - Required - Unique command ID. - **address** (String) - Required - Target aggregate ID. - **payload** (Object) - Required - The command data. - **metadata** (Object) - Optional - Correlation and causation IDs for tracking. ``` -------------------------------- ### Implement OrderService Stomaton Source: https://github.com/hnaderi/edomata/blob/main/docs/tutorials/1-2_cqrs.md Implements a service using the Stomaton pattern to handle commands and publish notifications. It uses the App router to modify state and emit events. ```scala object OrderService extends Order.Service[Command, Notification] { import cats.Monad def apply[F[_] : Monad]: App[F, Unit] = App.router{ case Command.Place(food, address) => for { ns <- App.modifyS(_.place(food, address)) _ <- App.publish(Notification.Received(food)) } yield () case Command.MarkAsCooking(cook: String) => for { ns <- App.modifyS(_.markAsCooking(cook)) _ <- App.publish(Notification.Cooking) } yield () case _ => ??? } } ``` -------------------------------- ### Outbox Consumer API Source: https://context7.com/hnaderi/edomata/llms.txt Pattern for processing integration events reliably using the OutboxConsumer. ```APIDOC ## POST /outbox/consume ### Description Processes outboxed notifications to trigger external system integrations (e.g., message brokers). ### Method POST ### Endpoint /outbox/consume ### Request Body - **handler** (Function) - Required - A function that processes the notification based on its type (e.g., AccountOpened, BalanceUpdated). ``` -------------------------------- ### Scala Imports for Edomata and Cats Source: https://github.com/hnaderi/edomata/blob/main/docs/tutorials/1-2_cqrs.md Imports necessary for using Edomata core functionalities, syntax extensions, and Cats Implicits for enhanced functionality. ```scala import edomata.core.* import edomata.syntax.all. // for convenient extension methods import cats.implicits. // to make life easier ``` -------------------------------- ### Decision Core API Source: https://context7.com/hnaderi/edomata/llms.txt Explains the Decision monad used for handling events, rejections, and state transitions in Edomata. ```APIDOC ## Decision Core API ### Description The `Decision` type is the fundamental building block for event-driven logic in Edomata. It represents a computation that can result in a value, a set of events, or a rejection. ### Method N/A (Functional Monad) ### Parameters - **value** (Any) - The result of the decision. - **events** (Chain) - The events to be persisted. - **rejection** (Any) - The error reason if the decision is rejected. ### Request Example ```scala val decision = for { _ <- Decision.accept("AccountOpened") val <- Decision.pure(100) } yield val ``` ### Response #### Success Response - **Accepted** (Decision) - Contains the resulting value and the list of events to be emitted. #### Response Example ```json { "status": "Accepted", "events": ["AccountOpened"], "result": 100 } ``` ``` -------------------------------- ### Add Edomata Core Dependency to Scala Build Source: https://github.com/hnaderi/edomata/blob/main/docs/tutorials/0_getting_started.md This snippet shows how to add the edomata-core library to your Scala project's build dependencies. It provides separate configurations for standard Scala and Scala.js projects. ```scala libraryDependencies += "dev.hnaderi" %% "edomata-core" % "@VERSION@" ``` ```scala libraryDependencies += "dev.hnaderi" %%% "edomata-core" % "@VERSION@" ``` -------------------------------- ### Integrate Decisions with Cats Typeclasses Source: https://github.com/hnaderi/edomata/blob/main/docs/tutorials/1-1_eventsourcing.md Demonstrates interoperability with Cats library features such as mapN, traverse, and sequence for handling collections of decisions. ```scala import cats.implicits.* val d8 = (d1, d7).mapN(_ + _) val d9 = List.range(1, 5).traverse(Decision.accept(_)) val d10 = Decision(List.range(1, 5)).sequence[List, Int] ``` -------------------------------- ### Define Domain Model and Transitions in Scala Source: https://context7.com/hnaderi/edomata/llms.txt This snippet demonstrates how to define an aggregate root using the DomainModel trait. It includes event definitions, rejection types, state transitions, and business logic for an account management system. ```scala import edomata.core.* import edomata.syntax.all.* import cats.implicits.* import cats.data.ValidatedNec enum Event { case Opened case Deposited(amount: BigDecimal) case Withdrawn(amount: BigDecimal) case Closed } enum Rejection { case ExistingAccount case NoSuchAccount case InsufficientBalance case NotSettled case AlreadyClosed } enum Account { case New case Open(balance: BigDecimal) case Closed def open: Decision[Rejection, Event, Account.Open] = this.decide { case Account.New => Decision.accept(Event.Opened) case _ => Decision.reject(Rejection.ExistingAccount) }.validate(_.mustBeOpen) def deposit(amount: BigDecimal): Decision[Rejection, Event, Account.Open] = this.perform(mustBeOpen.toDecision.flatMap { account => if amount > 0 then Decision.accept(Event.Deposited(amount)) else Decision.reject(Rejection.InsufficientBalance) }).validate(_.mustBeOpen) def withdraw(amount: BigDecimal): Decision[Rejection, Event, Account.Open] = this.perform(mustBeOpen.toDecision.flatMap { account => if account.balance >= amount && amount > 0 then Decision.accept(Event.Withdrawn(amount)) else Decision.reject(Rejection.InsufficientBalance) }).validate(_.mustBeOpen) def close: Decision[Rejection, Event, Account] = this.perform(mustBeOpen.toDecision.flatMap { account => if account.balance == 0 then Event.Closed.accept else Decision.reject(Rejection.NotSettled) }) private def mustBeOpen: ValidatedNec[Rejection, Account.Open] = this match { case o @ Account.Open(_) => o.validNec case Account.New => Rejection.NoSuchAccount.invalidNec case Account.Closed => Rejection.AlreadyClosed.invalidNec } } object Account extends DomainModel[Account, Event, Rejection] { def initial: Account = Account.New def transition: Event => Account => ValidatedNec[Rejection, Account] = { case Event.Opened => _ => Account.Open(0).validNec case Event.Deposited(b) => _.mustBeOpen.map(s => s.copy(balance = s.balance + b)) case Event.Withdrawn(b) => _.mustBeOpen.map(s => s.copy(balance = s.balance - b)) case Event.Closed => _ => Account.Closed.validNec } } ``` -------------------------------- ### Decision Model Operations Source: https://github.com/hnaderi/edomata/blob/main/docs/tutorials/1-1_eventsourcing.md Explains how to create, compose, and transform Decision objects using the Edomata library. ```APIDOC ## Decision Model Operations ### Description The Decision model represents pure programs that can accept events, reject with errors, or remain indecisive. It supports composition and transformation via monadic operations. ### Usage Decisions are created using `Decision.accept`, `Decision.reject`, or `Decision.pure`. ### Examples ```scala // Creating decisions val d1 = Decision(1) val d2 = Decision.accept("Missile Launched!") val d3 = Decision.reject("No remained missiles to launch!") // Composition val d4 = d1.map(_ * 2) val d5 = d2 >> d1 ``` ### For-Comprehension Decisions can be composed using standard Scala for-comprehensions: ```scala val d7 = for { i <- Decision.pure(1) _ <- Decision.accept("A") j <- Decision.acceptReturn(i * 2)("D", "E") } yield i + j ``` ``` -------------------------------- ### Stomaton CQRS Service Layer in Scala Source: https://context7.com/hnaderi/edomata/llms.txt Implements a CQRS-style service layer using Stomaton, defining commands, notifications, and routing logic. It demonstrates how to modify state, publish notifications, and handle command processing. Dependencies include Cats Effect and Edomata core libraries. ```scala import edomata.core.* import cats.Monad import cats.effect.IO import java.time.Instant // Define commands and notifications enum Command { case Place(food: String, address: String) case MarkAsCooking(cook: String) case MarkAsCooked case MarkAsDelivering(unit: String) case MarkAsDelivered case Rate(score: Int) } enum Notification { case Received(food: String) case Cooking case Cooked case Delivering case Delivered } // Create CQRS service object OrderService extends Order.Service[Command, Notification] { def apply[F[_]: Monad]: App[F, Unit] = App.router { case Command.Place(food, address) => for { ns <- App.modifyS(_.place(food, address)) _ <- App.publish(Notification.Received(food)) } yield () case Command.MarkAsCooking(cook) => for { ns <- App.modifyS(_.markAsCooking(cook)) _ <- App.publish(Notification.Cooking) } yield () case Command.MarkAsCooked => for { ns <- App.modifyS(_.markAsCooked) _ <- App.publish(Notification.Cooked) } yield () case _ => App.reject(Rejection.InvalidRequest) } } // Testing the Stomaton val srv = OrderService[cats.Id] val scenario = srv.run( CommandMessage( id = "cmd-001", time = Instant.now, address = "order-123", payload = Command.Place("taco", "home") ), Order.Empty // Initial state ) scenario.result // Right((Placed(taco, home, New), ())) senario.notifications // Chain(Received(taco)) ``` -------------------------------- ### Compose Decisions with Monadic Operations Source: https://github.com/hnaderi/edomata/blob/main/docs/tutorials/1-1_eventsourcing.md Shows how to chain multiple decisions using map, flatMap (via >>), and for-comprehensions. This allows for complex event-driven workflows. ```scala val d4 = d1.map(_ * 2) val d5 = d2 >> d1 val d6 = d5 >> d3 val d7 = for { i <- Decision.pure(1) _ <- Decision.accept("A") _ <- Decision.accept("B", "C") j <- Decision.acceptReturn(i * 2)("D", "E") } yield i + j ``` -------------------------------- ### Scala Edomata Decision Logic for Bank Account Source: https://github.com/hnaderi/edomata/blob/main/docs/tutorials/1-1_eventsourcing.md Implements domain logic for bank account operations using Edomata's Decision API in Scala. It defines methods for opening, closing, withdrawing, and depositing, including validation and rejection handling. ```scala import edomata.core.* import edomata.syntax.all.* import cats.implicits.* import cats.data.ValidatedNec enum Account { case New case Open(balance: BigDecimal) case Close def open : Decision[Rejection, Event, Open] = this.decide { // 1 case New => Decision.accept(Event.Opened) case _ => Decision.reject(Rejection.ExistingAccount) }.validate(_.mustBeOpen) // 2 def close : Decision[Rejection, Event, Account] = this.perform(mustBeOpen.toDecision.flatMap { account => // 3, 4 if account.balance == 0 then Event.Closed.accept else Decision.reject(Rejection.NotSettled) }) def withdraw(amount: BigDecimal): Decision[Rejection, Event, Open] = this.perform(mustBeOpen.toDecision.flatMap { account => if account.balance >= amount && amount > 0 then Decision.accept(Event.Withdrawn(amount)) else Decision.reject(Rejection.InsufficientBalance) // We can model rejections to have values, which helps a lot for showing error messages, but it's out of scope for this document }).validate(_.mustBeOpen) def deposit(amount: BigDecimal): Decision[Rejection, Event, Open] = this.perform(mustBeOpen.toDecision.flatMap { account => if amount > 0 then Decision.accept(Event.Deposited(amount)) else Decision.reject(Rejection.BadRequest) }).validate(_.mustBeOpen) private def mustBeOpen : ValidatedNec[Rejection, Open] = this match { // 5 case o@Open(_) => o.validNec case New => Rejection.NoSuchAccount.invalidNec case Close => Rejection.AlreadyClosed.invalidNec } } ``` -------------------------------- ### Event Sourcing Verification Logic Source: https://github.com/hnaderi/edomata/blob/main/docs/principles/index.md A visual representation of the logic used to determine if a system component qualifies as event-sourced based on the presence of event-based reasoning. ```plantuml start :system changes its state; if (why?) then (I have its reason as an event) #palegreen:verified as event sourced; else (It depends) #pink:not event sourced; endif ``` -------------------------------- ### Journal and Repository Access Source: https://context7.com/hnaderi/edomata/llms.txt Methods for reading event journals and aggregate state from the Edomata backend. ```APIDOC ## GET /journal/readAll ### Description Reads the entire event journal from the beginning. ### Method GET ### Endpoint /journal/readAll ### Response #### Success Response (200) - **events** (Stream[JournalEntry[Event]]) - A stream of all events in the journal. ## GET /journal/readStream ### Description Reads events for a specific aggregate stream. ### Method GET ### Endpoint /journal/readStream/{aggregateId} ### Parameters #### Path Parameters - **aggregateId** (String) - Required - The unique identifier of the aggregate. ## GET /repository/get ### Description Retrieves the current state of an aggregate. ### Method GET ### Endpoint /repository/get/{aggregateId} ### Parameters #### Path Parameters - **aggregateId** (String) - Required - The unique identifier of the aggregate. ### Response #### Success Response (200) - **state** (AggregateState) - The current state of the aggregate. ``` -------------------------------- ### Read Event Journal Streams Source: https://github.com/hnaderi/edomata/blob/main/docs/tutorials/3_processes.md Shows methods for reading event journals, including full history, specific streams, and filtering by sequence numbers. ```scala def all = backend.journal.readAll def singleStream = backend.journal.readStream("interesting-stream") def allAfter = backend.journal.readAllAfter(100) def singleBefore = backend.journal.readStreamBefore("interesting-stream" ,100) ``` -------------------------------- ### Scala Order Aggregate Root with Domain Logic Source: https://github.com/hnaderi/edomata/blob/main/docs/tutorials/1-2_cqrs.md Models the Order aggregate root using a Scala enum, including methods like place and markAsCooking that encapsulate domain logic and return EitherNec to handle potential rejections. ```scala enum Order { case Empty case Placed(food: String, address: String, status: OrderStatus = OrderStatus.New) case Delivered(rating: Int) def place(food: String, address: String) = this match { case Empty => Placed(food, address).asRight case _ => Rejection.ExistingOrder.leftNec } def markAsCooking(cook: String) = this match { case st@Placed(_, _, OrderStatus.New) => st.copy(status = OrderStatus.Cooking(cook)).asRight case _ => Rejection.InvalidRequest.leftNec } // other logics from business } ``` -------------------------------- ### Scala Order CQRSModel Definition Source: https://github.com/hnaderi/edomata/blob/main/docs/tutorials/1-2_cqrs.md Defines the initial state for the Order aggregate using Edomata's CQRSModel, specifying Empty as the initial state to ensure model consistency and facilitate future evolution. ```scala object Order extends CQRSModel[Order, Rejection] { def initial = Empty } ``` -------------------------------- ### Scala 3 Enum for Rejection Scenarios Source: https://github.com/hnaderi/edomata/blob/main/docs/tutorials/1-1_eventsourcing.md Models various rejection scenarios that can occur during bank account operations using Scala 3 enums. This includes cases like existing accounts, insufficient balance, and accounts not being settled. ```scala enum Rejection { case ExistingAccount case NoSuchAccount case InsufficientBalance case NotSettled case AlreadyClosed case BadRequest } ```