### Example: Sending Notification Emails Source: https://github.com/beyond-scale-group/edomata/blob/main/docs/tutorials/processes.md An example demonstrating how to use OutboxConsumer to send notification emails based on the type of outboxed item payload. It includes looking up email addresses and sending emails using an emailService. ```scala def emailPublisher: Stream[IO, Nothing] = OutboxConsumer(backend) { item => item.payload match { case Notification.AccountOpened(accountId) => emailService.send( to = lookupEmail(accountId), subject = "Welcome!", body = "Your account has been opened." ) case Notification.BalanceUpdated(accountId, balance) => emailService.send( to = lookupEmail(accountId), subject = "Balance Updated", body = s"Your new balance is $balance" ) case _ => IO.unit } } ``` -------------------------------- ### Disable Automatic Setup with skipSetup Source: https://github.com/beyond-scale-group/edomata/blob/main/docs/backends/skunk.md Configure the Skunk driver with skipSetup = true when using external migration tools like Flyway to manage DDL. ```scala val buildBackend = Backend .builder(AccountService) .use(SkunkDriver.from(PGNaming.prefixed("accounts"), pool, skipSetup = true)) .inMemSnapshot(200) .build ``` -------------------------------- ### Create a Stomaton Service for Order Management Source: https://github.com/beyond-scale-group/edomata/blob/main/docs/tutorials/cqrs.md Implement an order service using Stomaton, defining how to handle commands like 'Place' and 'MarkAsCooking'. This example demonstrates direct state modification using `App.modifyS` and publishing notifications. ```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 _ => ??? // other command handling logic } } ``` -------------------------------- ### Install integrated Doobie modules Source: https://github.com/beyond-scale-group/edomata/blob/main/docs/backends/doobie.md Add optional integrated modules for Circe or uPickle support. ```scala libraryDependencies += "dev.bsg" %% "edomata-doobie-circe" % "@VERSION@" libraryDependencies += "dev.bsg" %% "edomata-doobie-upickle" % "@VERSION@" ``` -------------------------------- ### Build Search Index from Journal Source: https://github.com/beyond-scale-group/edomata/blob/main/docs/tutorials/processes.md An example of building a search index by continuously reading events from the journal after a checkpoint. It updates the index and saves progress, allowing for resilient, resumable processing. ```scala def searchIndexer: Stream[IO, Unit] = backend.journal.readAllAfter(lastProcessedSequence).evalMap { event => for { _ <- updateSearchIndex(event) _ <- saveCheckpoint(event.sequence) } yield () } ``` -------------------------------- ### Compile Application with SkunkDriver Source: https://github.com/beyond-scale-group/edomata/blob/main/docs/backends/skunk.md Compile your Edomata application using SkunkDriver, configuring snapshotting and retry policies. This example shows event sourcing. ```scala val app = ??? // your application from previous chapter val pool : Resource[IO, Session[IO]] = ??? // create your own session pool val buildBackend = Backend .builder(AccountService) // 1 .use(SkunkDriver("domainname", pool)) // 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) } ``` -------------------------------- ### Disable automatic setup Source: https://github.com/beyond-scale-group/edomata/blob/main/docs/backends/doobie.md Prevent the driver from executing DDL when using external migration tools. ```scala val buildBackend = Backend .builder(AccountService) .use(DoobieDriver.from(PGNaming.prefixed("accounts"), trx, skipSetup = true)) .inMemSnapshot(200) .build ``` -------------------------------- ### Install Doobie dependencies Source: https://github.com/beyond-scale-group/edomata/blob/main/docs/backends/doobie.md Add the core Doobie library to your Scala project dependencies. ```scala libraryDependencies += "dev.bsg" %% "edomata-doobie" % "@VERSION@" ``` -------------------------------- ### Read All Journal Events Source: https://github.com/beyond-scale-group/edomata/blob/main/docs/tutorials/processes.md Retrieves all events from the journal starting from the beginning. Use this for a complete system history. ```scala def all = backend.journal.readAll ``` -------------------------------- ### Order Fulfillment Process Manager Source: https://github.com/beyond-scale-group/edomata/blob/main/docs/tutorials/processes.md An example of a Process Manager orchestrating a multi-step workflow. It listens for 'OrderPlaced' notifications, reserves inventory, charges payment, and schedules delivery. It also handles 'PaymentFailed' notifications for compensation. ```scala // Example: Order fulfillment process def orderFulfillment: Stream[IO, Unit] = OutboxConsumer(backend) { item => item.payload match { case Notification.OrderPlaced(orderId, items) => for { // Reserve inventory _ <- inventoryService.reserve(items) // Charge payment _ <- paymentService.charge(orderId) // Schedule delivery _ <- deliveryService.schedule(orderId) } yield () case Notification.PaymentFailed(orderId) => // Compensate: release inventory inventoryService.release(orderId) case _ => IO.unit } } ``` -------------------------------- ### Initialize Edomata Backend with Skunk Source: https://github.com/beyond-scale-group/edomata/blob/main/docs/tutorials/backends.md Demonstrates the complete lifecycle of creating a database connection pool, building the backend, and executing a command. ```scala import cats.effect.* import edomata.backend.* import edomata.backend.eventsourcing.Backend import edomata.skunk.* // or edomata.doobie.* import skunk.Session // Your domain from previous chapters // Account, Event, Rejection, Notification, AccountService... object Main extends IOApp.Simple { def run: IO[Unit] = { // 1. Create database connection pool val pool: Resource[IO, Session[IO]] = Session.pooled( host = "localhost", port = 5432, user = "postgres", database = "postgres", password = Some("postgres"), max = 10 ) // 2. Create backend val buildBackend = Backend .builder(AccountService) // 1 .use(SkunkDriver("account", pool)) // 2 .persistedSnapshot(maxInMem = 200) // 3 .build buildBackend.use { backend => // 3. Compile your service val service = backend.compile(AccountService[IO]) // 4. Use it! for { result <- service( CommandMessage( id = "cmd-1", time = java.time.Instant.now(), address = "account-123", payload = Command.Open ) ) _ <- IO.println(s"Result: $result") } yield () } } } ``` -------------------------------- ### Decision Creation with Syntax Source: https://github.com/beyond-scale-group/edomata/blob/main/docs/tutorials/eventsourcing.md Shows how to create Decisions using extension methods provided by `edomata.syntax.all`. This allows for more concise syntax like `1.asDecision`. ```scala import edomata.syntax.all.* 1.asDecision "Missile Launched!".accept "No remained missiles to launch!".reject ``` -------------------------------- ### Import SaaS module Source: https://github.com/beyond-scale-group/edomata/blob/main/docs/tutorials/saas.md Import the necessary members from the edomata.saas package. ```scala import edomata.saas.* ``` -------------------------------- ### Implement Service Logic with State Machine Source: https://github.com/beyond-scale-group/edomata/blob/main/docs/tutorials/saas.md Illustrates how to implement service logic using a state machine pattern within a `guardedRouter`. Handles commands like `ProductCommand.Publish` by checking and updating entity state. ```scala import SaaS.* def apply[F[_]: Monad](): App[F, Unit] = guardedRouter { case ProductCommand.Publish => (CrudAction.Update, for state <- entityState _ <- state match case CrudState.Active(tid, oid, p) if p.status == ProductStatus.Draft || p.status == ProductStatus.Archived => set(CrudState.Active(tid, oid, p.copy(status = ProductStatus.Published))) >> publish(ProductNotification.Published) case CrudState.Active(_, _, p) => reject(ProductRejection.InvalidTransition(p.status.toString, "Published")) case _ => reject(ProductRejection.NotFound) yield ()) ``` -------------------------------- ### Run and Test a Stomaton Scenario Source: https://github.com/beyond-scale-group/edomata/blob/main/docs/tutorials/cqrs.md Instantiate and run a Stomaton service with a specific command and initial state. This allows for testing happy paths, error cases, and state transitions. ```scala import java.time.Instant // as we've written our service definition in a tagless style, // we are free to provide any type param that satisfies required type-classes val srv = OrderService[cats.Id] // or any other effect type val scenario1 = srv.run( CommandMessage( id = "cmd id", time = Instant.MIN, address = "aggregate id", payload = Command.Place("taco", "home") ), Order.Empty // state to run command on ) ``` -------------------------------- ### Define Backend Instance Source: https://github.com/beyond-scale-group/edomata/blob/main/docs/tutorials/processes.md Defines a backend instance with specified types for IO, Account, Event, Rejection, and Notification. This is a placeholder and requires a concrete implementation. ```scala def backend : Backend[IO, Account, Event, Rejection, Notification] = ??? ``` -------------------------------- ### Get Current Aggregate State from Repository Source: https://github.com/beyond-scale-group/edomata/blob/main/docs/tutorials/processes.md Retrieves the current computed state of a single aggregate root from the repository. This is a direct read of the latest state without processing events. ```scala def current : IO[AggregateState[Account, Event, Rejection]] = backend.repository.get("interesting-stream") ``` -------------------------------- ### Run Migrations with Backend Drivers Source: https://github.com/beyond-scale-group/edomata/blob/main/docs/tutorials/migrations.md Execute migrations before initializing the backend. These operations are idempotent and safe to run on application startup. ```scala import edomata.skunk.SkunkMigrations for result <- SkunkMigrations.run(naming, pool, allMigrations) _ <- IO.println(s"Applied: ${result.applied}, Skipped: ${result.skipped}") driver <- SkunkDriver.from(naming, pool) // Build backend with latest event codec only yield driver ``` ```scala import edomata.doobie.DoobieMigrations for result <- DoobieMigrations.run(naming, transactor, allMigrations) _ <- IO.println(s"Applied: ${result.applied}, Skipped: ${result.skipped}") driver <- DoobieDriver.from(naming, transactor) yield driver ``` -------------------------------- ### Implement Account Opening Logic with Edomata Decision Source: https://github.com/beyond-scale-group/edomata/blob/main/docs/tutorials/eventsourcing.md Implement the logic for opening a new account using Edomata's `Decision` API. This handles the transition from the `New` state and rejects if the account already exists. ```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 { case New => Decision.accept(Event.Opened) case _ => Decision.reject(Rejection.ExistingAccount) }.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) }) 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) }).validate(_.mustBeOpen) def deposit(amount: BigDecimal): Decision[Rejection, Event, Open] = this.perform(mustBeOpen.toDecision.flatMap { case account@Open(_) => if amount > 0 then Decision.accept(Event.Deposited(amount)) else Decision.reject(Rejection.BadRequest) case _ => Decision.reject(Rejection.NoSuchAccount) }).validate(_.mustBeOpen) private def mustBeOpen : ValidatedNec[Rejection, Open] = this match { case o@Open(_) => o.validNec case New => Rejection.NoSuchAccount.invalidNec case Close => Rejection.AlreadyClosed.invalidNec } } ``` -------------------------------- ### Define Rejection Types with Scala 3 Enum Source: https://github.com/beyond-scale-group/edomata/blob/main/docs/tutorials/cqrs.md Model potential rejections or errors in domain logic using a Scala 3 enum. This example includes ExistingOrder, NoSuchOrder, and a general InvalidRequest, with a note to make rejections more specific in real applications. ```scala enum Rejection { case ExistingOrder case NoSuchOrder case InvalidRequest // this should be more fine grained in real world applications } ``` -------------------------------- ### Configure Build Dependencies Source: https://github.com/beyond-scale-group/edomata/blob/main/docs/tutorials/saas.md Add the edomata-saas dependency to your build.sbt file to enforce the use of the guarded DSL. ```scala // build.sbt libraryDependencies += "dev.bsg" %% "edomata-saas" % "@VERSION@" // Do NOT add edomata-core -- use edomata.saas.* imports only ``` -------------------------------- ### Flyway Migration: Configure SkunkCQRSDriver Source: https://github.com/beyond-scale-group/edomata/blob/main/docs/tutorials/saas.md Demonstrates how to configure the `SkunkCQRSDriver` for Flyway migrations by setting `skipSetup = true`. This is used when DDL is managed separately. ```scala SkunkCQRSDriver[IO](PGNaming.prefixed("catalog"), pool, skipSetup = true) ``` -------------------------------- ### Import Skunk Package Source: https://github.com/beyond-scale-group/edomata/blob/main/docs/backends/skunk.md Import the necessary Skunk package for Edomata integration. ```scala import edomata.skunk.* ``` -------------------------------- ### SQL: Custom Read-Model Projection Table Source: https://github.com/beyond-scale-group/edomata/blob/main/docs/tutorials/saas.md Defines the SQL `CREATE TABLE` statement for a custom read-model projection table named `products_read`. Includes columns for product details and indexing for efficient tenant-scoped queries. ```sql CREATE TABLE IF NOT EXISTS products_read ( id text NOT NULL PRIMARY KEY, tenant_id text NOT NULL, name text NOT NULL, description text NOT NULL, price_cents bigint NOT NULL, currency text NOT NULL, status text NOT NULL, deleted boolean NOT NULL DEFAULT false, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now() ); -- Mandatory: all tenant-scoped queries filter on this CREATE INDEX IF NOT EXISTS products_read_tenant_idx ON products_read (tenant_id) WHERE deleted = false; -- Optional: status filtering within a tenant CREATE INDEX IF NOT EXISTS products_read_tenant_status_idx ON products_read (tenant_id, status) WHERE deleted = false; ``` -------------------------------- ### Configure Backend Builder Source: https://github.com/beyond-scale-group/edomata/blob/main/docs/tutorials/saas.md Connect the SaaS service domain to the backend builder using the service's domain field. ```scala import edomata.saas.* val backend = Backend .builder(TodoService.domain) .use(driver) .build ``` -------------------------------- ### Compile application to a service Source: https://github.com/beyond-scale-group/edomata/blob/main/docs/backends/doobie.md Build a backend using the Doobie driver and compile the application service. ```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) } ``` -------------------------------- ### Implement AccountService Edomaton Source: https://github.com/beyond-scale-group/edomata/blob/main/docs/tutorials/eventsourcing.md Create a service using App.router to handle commands, update state via decisions, and publish notifications. ```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 } } ``` -------------------------------- ### Import Doobie Source: https://github.com/beyond-scale-group/edomata/blob/main/docs/backends/doobie.md Import the necessary Doobie components for Edomata. ```scala import edomata.doobie.* ``` -------------------------------- ### Add SaaS module dependency Source: https://github.com/beyond-scale-group/edomata/blob/main/docs/tutorials/saas.md Include the edomata-saas library in your project's build configuration. ```scala libraryDependencies += "dev.bsg" %% "edomata-saas" % "@VERSION@" ``` -------------------------------- ### Execute an Edomaton Source: https://github.com/beyond-scale-group/edomata/blob/main/docs/tutorials/eventsourcing.md Demonstrates executing an Edomaton using both the Id monad for simple testing and the IO monad for effectful operations. ```scala // as we've written our service definition in a tagless style, // we are free to provide any type param that satisfies required type-classes import cats.Id val obtained = AccountService[Id].execute(scenario1) // or even use a real IO monad if needed import cats.effect.IO AccountService[IO].execute(scenario1) ``` -------------------------------- ### Control PostgreSQL naming Source: https://github.com/beyond-scale-group/edomata/blob/main/docs/backends/doobie.md Use PGNaming directly to toggle between schema and prefix modes. ```scala import edomata.backend.PGNaming // Schema mode (default behavior) DoobieDriver.from(PGNaming.schema("domainname"), trx) // Prefix mode DoobieDriver.from(PGNaming.prefixed("domainname"), trx) ``` -------------------------------- ### Imports for Edomata Source: https://github.com/beyond-scale-group/edomata/blob/main/docs/tutorials/eventsourcing.md Import necessary Edomata core components and syntax for convenient extension methods. ```scala import edomata.core.* import edomata.syntax.all.* // for convenient extension methods ``` -------------------------------- ### Custom Table Naming with PGNaming Source: https://github.com/beyond-scale-group/edomata/blob/main/docs/backends/skunk.md Utilize PGNaming for more granular control over schema and prefix modes when configuring the Skunk driver. ```scala import edomata.backend.PGNaming // Schema mode (default behavior) SkunkDriver.from(PGNaming.schema("domainname"), pool) // Prefix mode SkunkDriver.from(PGNaming.prefixed("domainname"), pool) ``` -------------------------------- ### Create EitherNec instances Source: https://github.com/beyond-scale-group/edomata/blob/main/docs/tutorials/cqrs.md Demonstrates the creation of EitherNec instances representing success (Right) and failure (Left) using literal values and extension methods. ```scala val e1 = Right(1) val e2 = "Missile Launched!".asRight val e3 = "No remained missiles to launch!".leftNec ``` -------------------------------- ### Add Edomata Skunk + Circe Backend to build.sbt Source: https://github.com/beyond-scale-group/edomata/blob/main/README.md Add this dependency to your build.sbt for a recommended PostgreSQL backend using Skunk and Circe for JSON handling. ```scala // With Skunk + Circe PostgreSQL backend (recommended) libraryDependencies += "dev.bsg" %% "edomata-skunk-circe" % "0.12.5" ``` -------------------------------- ### Configure table prefix mode Source: https://github.com/beyond-scale-group/edomata/blob/main/docs/backends/doobie.md Use prefix-based naming instead of schema-based naming for PostgreSQL tables. ```scala import edomata.backend.PGNamespace val buildBackend = Backend .builder(AccountService) .use(DoobieDriver.from(PGNamespace.prefixed("domainname"), trx)) .inMemSnapshot(200) .build ``` -------------------------------- ### Define Backend Codecs Source: https://github.com/beyond-scale-group/edomata/blob/main/docs/backends/skunk.md Provide backend codecs for Event, Notification, and State using Circe. Choose between .jsonb or .json formats. ```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 ``` -------------------------------- ### Test Domain Model Logic Source: https://github.com/beyond-scale-group/edomata/blob/main/docs/tutorials/eventsourcing.md Demonstrates testing pure domain logic by invoking state transitions directly. ```scala Account.New.open Account.Open(10).deposit(2) Account.Open(5).close Account.New.open.flatMap(_.close) ``` -------------------------------- ### Configure Table Prefix Mode Source: https://github.com/beyond-scale-group/edomata/blob/main/docs/backends/skunk.md Use SkunkDriver.from with PGNamespace.prefixed to store all tables in a single schema with prefixes, useful for migration tools. ```scala import edomata.backend.PGNamespace val buildBackend = Backend .builder(AccountService) .use(SkunkDriver.from(PGNamespace.prefixed("domainname"), pool)) .inMemSnapshot(200) .build ``` -------------------------------- ### SQL: CQRS Backend Tables Source: https://github.com/beyond-scale-group/edomata/blob/main/docs/tutorials/saas.md Defines SQL `CREATE TABLE` statements for CQRS backend tables: `catalog_states` for aggregate states, `catalog_outbox` for event notifications, and `catalog_commands` for command idempotency. ```sql -- States table: stores CrudState[Product] as JSONB per aggregate CREATE TABLE IF NOT EXISTS catalog_states ( id text NOT NULL, "version" int8 NOT NULL, state jsonb NOT NULL, CONSTRAINT catalog_states_pk PRIMARY KEY (id) ); -- Outbox table: stores ProductNotification for async handlers CREATE TABLE IF NOT EXISTS catalog_outbox ( seqnr bigserial NOT NULL, stream text NOT NULL, correlation text NULL, causation text NULL, payload jsonb NOT NULL, created timestamptz NOT NULL, published timestamptz NULL, CONSTRAINT catalog_outbox_pk PRIMARY KEY (seqnr) ); -- Commands table: idempotency deduplication CREATE TABLE IF NOT EXISTS catalog_commands ( id text NOT NULL, "time" timestamptz NOT NULL, address text NOT NULL, CONSTRAINT catalog_commands_pk PRIMARY KEY (id) ``` -------------------------------- ### Compose EitherNec instances Source: https://github.com/beyond-scale-group/edomata/blob/main/docs/tutorials/cqrs.md Shows how to compose EitherNec instances using map for transformations on success values and the monadic bind (>>) for sequential operations. ```scala val e4 = e1.map(_ * 2) val e5 = e2 >> e1 ``` -------------------------------- ### Add Edomata Core for Scala.js or Scala Native Source: https://github.com/beyond-scale-group/edomata/blob/main/README.md Use `%%%` instead of `%%` when adding Edomata core library dependencies for Scala.js or Scala Native projects. ```scala libraryDependencies += "dev.bsg" %%% "edomata-core" % "0.12.5" ``` -------------------------------- ### Define a CQRS Service with SaaS Source: https://github.com/beyond-scale-group/edomata/blob/main/docs/tutorials/saas.md Implement a service using SaaSCQRSService to automatically enforce authorization guards on commands. ```scala import edomata.saas.* import cats.Monad // Your business types case class Todo(title: String, completed: Boolean) enum TodoCommand: case Create(title: String) case Complete case Delete // Your CQRS model (state = CrudState[Todo]) object TodoModel extends edomata.core.CQRSModel[CrudState[Todo], String]: def initial = CrudState.NonExistent import TodoModel.given // Auth policy given AuthPolicy[CallerIdentity] = RoleBasedPolicy { case CrudAction.Create => Set("todo:write") case CrudAction.Read => Set("todo:read") case CrudAction.Update => Set("todo:write") case CrudAction.Delete => Set("todo:admin") } // Your service -- guards are automatic object TodoService extends SaaSCQRSService[ CallerIdentity, TodoCommand, Todo, String, String ](mkRejection = identity): import SaaS.* def apply[F[_]: Monad](): App[F, Unit] = guardedRouter { case TodoCommand.Create(title) => (CrudAction.Create, for a <- auth _ <- set(CrudState.Active(a.tenantId, a.userId, Todo(title, false))) yield ()) case TodoCommand.Complete => (CrudAction.Update, for _ <- modifyS { case CrudState.Active(tid, oid, todo) => Right(CrudState.Active(tid, oid, todo.copy(completed = true))) case other => Left(cats.data.NonEmptyChain.one("Not found")) } yield ()) case TodoCommand.Delete => (CrudAction.Delete, for state <- entityState _ <- state match case CrudState.Active(tid, oid, _) => set(CrudState.Deleted(tid, oid)) case _ => reject("Not found") yield ()) } ``` -------------------------------- ### Import necessary libraries for Edomata Source: https://github.com/beyond-scale-group/edomata/blob/main/docs/tutorials/cqrs.md Imports required for using Edomata's core functionalities, syntax extensions, and Cats utilities. ```scala import edomata.core.* import edomata.syntax.all.* // for convenient extension methods import cats.implicits.* // to make life easier ``` -------------------------------- ### Add Edomata dependency to build Source: https://github.com/beyond-scale-group/edomata/blob/main/docs/tutorials/getting-started.md Include the core library in your Scala project build configuration. ```scala libraryDependencies += "dev.bsg" %% "edomata-core" % "@VERSION@" ``` ```scala libraryDependencies += "dev.bsg" %%% "edomata-core" % "@VERSION@" ``` -------------------------------- ### Define SaaSCommand Source: https://github.com/beyond-scale-group/edomata/blob/main/docs/tutorials/saas.md A wrapper for business commands that includes the authentication context. ```scala final case class SaaSCommand[Auth, +C]( auth: Auth, payload: C ) ``` -------------------------------- ### Define Domain Types for Product Catalog Source: https://github.com/beyond-scale-group/edomata/blob/main/docs/tutorials/saas.md Defines case classes and enums for representing products, their statuses, and commands within the catalog domain. Includes context for API key authentication. ```scala case class ApiKeyContext( apiKey: String, tenantId: TenantId, ownerId: UserId, scopes: Set[String] // "catalog:read", "catalog:write", "catalog:admin" ) enum ProductStatus: case Draft, Published, Archived case class Product( name: String, description: String, priceCents: Long, currency: String, status: ProductStatus ) enum ProductCommand: case Create(name: String, description: String, priceCents: Long, currency: String) case UpdateDetails(name: String, description: String) case UpdatePrice(priceCents: Long, currency: String) case Publish, Archive, Delete enum ProductRejection: case NotFound case AlreadyExists case InvalidTransition(from: String, to: String) case InvalidPrice(reason: String) case Unauthorized(reason: String) ``` -------------------------------- ### Add Edomata Core Library to build.sbt Source: https://github.com/beyond-scale-group/edomata/blob/main/README.md Include this dependency in your build.sbt file to use the core Edomata library for JVM, Scala.js, or Scala Native projects. ```scala // Core library libraryDependencies += "dev.bsg" %% "edomata-core" % "0.12.5" ``` -------------------------------- ### Basic Decision Creation Source: https://github.com/beyond-scale-group/edomata/blob/main/docs/tutorials/eventsourcing.md Demonstrates the creation of indecisive, accepted, and rejected Decisions. `Decision(1)` creates an indecisive decision returning `1`. `Decision.accept(...)` accepts with an event, and `Decision.reject(...)` rejects with an error. ```scala val d1 = Decision(1) val d2 = Decision.accept("Missile Launched!") val d3 = Decision.reject("No remained missiles to launch!") ``` -------------------------------- ### Define a RequestContext for testing Source: https://github.com/beyond-scale-group/edomata/blob/main/docs/tutorials/eventsourcing.md Initializes a RequestContext with a command and aggregate state for use in testing scenarios. ```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 ) ``` -------------------------------- ### Visualize Event Sourcing Logic Source: https://github.com/beyond-scale-group/edomata/blob/main/docs/principles/index.md A PlantUML diagram illustrating the decision logic for determining if a system component is event-sourced. ```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; ``` -------------------------------- ### Chain and Compose Migrations Source: https://github.com/beyond-scale-group/edomata/blob/main/docs/tutorials/migrations.md Define multiple migrations as a list or compose them using the andThen method. ```scala val v2ToV3 = EventMigration[EventV2, EventV3]( "002", "Add description to Created" )( decode = io.circe.jawn.decode[EventV2](_).leftMap(_.getMessage), transform = { case EventV2.Created(name) => EventV3.Created(name, description = "") case EventV2.PriceUpdated(price, currency) => EventV3.PriceUpdated(price, currency) }, encode = _.asJson.noSpaces ) val allMigrations = List(v1ToV2, v2ToV3) ``` ```scala val v1ToV3 = v1ToV2.andThen(v2ToV3) ``` -------------------------------- ### Decision Composition with Cats Source: https://github.com/beyond-scale-group/edomata/blob/main/docs/tutorials/eventsourcing.md Shows how to compose Decisions using Cats library functions like `mapN` for combining results and `traverse` for applying an operation to a collection 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] ``` -------------------------------- ### Implement Domain Logic with EitherNec in Scala Source: https://github.com/beyond-scale-group/edomata/blob/main/docs/tutorials/cqrs.md Implement domain logic for placing and marking orders as cooking using Scala's `EitherNec` for error handling. This approach returns the new state directly, contrasting with event sourcing. ```scala import edomata.core.* import cats.implicits.* import cats.data.ValidatedNec 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 } ``` -------------------------------- ### Define a Domain Model and Transitions Source: https://github.com/beyond-scale-group/edomata/blob/main/docs/tutorials/eventsourcing.md Implements the DomainModel trait to define initial state and event-based transitions for an Account aggregate. ```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 } } ``` -------------------------------- ### Test Domain Logic with cats.Id Source: https://github.com/beyond-scale-group/edomata/blob/main/docs/tutorials/saas.md Use cats.Id to execute domain logic in a pure, synchronous environment without requiring a database or IO. ```scala import cats.Id import edomata.core.* import edomata.saas.* val dsl = SaaSCQRSDomainDSL[ApiKeyContext, ProductCommand, Product, ProductRejection, ProductNotification](mkRejection) val app: dsl.App[Id, Unit] = dsl.guardedRouter { /* ... */ } // Run against a state with a caller context val cmd = CommandMessage("cmd-1", Instant.now(), "product-1", SaaSCommand(callerA, ProductCommand.Publish)) val result = app.run(cmd, activeDraftState) // Assert on result assert(result.result.isRight) assertEquals(result.notifications, Chain(ProductNotification.Published)) ``` -------------------------------- ### Run an Edomaton for raw response Source: https://github.com/beyond-scale-group/edomata/blob/main/docs/tutorials/eventsourcing.md Executes an Edomaton to retrieve the raw Response model instead of the processed result. ```scala AccountService[Id].run(scenario1) ``` -------------------------------- ### Add GitHub Packages Resolver to build.sbt Source: https://github.com/beyond-scale-group/edomata/blob/main/README.md Configure your build.sbt to resolve artifacts published to GitHub Packages by the Beyond Scale Group for Edomata. ```scala resolvers += "GitHub Packages - edomata" at "https://maven.pkg.github.com/beyond-scale-group/edomata" ``` -------------------------------- ### Define Rejection Scenarios in Scala Source: https://github.com/beyond-scale-group/edomata/blob/main/docs/tutorials/eventsourcing.md Model potential reasons why a command might fail. These are expected business outcomes, not exceptions. Rejections can optionally carry data for richer error messages. ```scala enum Rejection { case ExistingAccount case NoSuchAccount case InsufficientBalance case NotSettled case AlreadyClosed case BadRequest } ``` -------------------------------- ### Map Guard Errors to Domain Rejections Source: https://github.com/beyond-scale-group/edomata/blob/main/docs/tutorials/saas.md Defines a private method `mkRejection` to map internal string-based guard errors to domain-specific `ProductRejection` types. This is used in the `ProductService` to ensure consistent error handling. ```scala private def mkRejection(msg: String): ProductRejection = if msg.contains("Tenant mismatch") || msg.contains("Entity not found") then ProductRejection.NotFound else ProductRejection.Unauthorized(msg) object ProductService extends SaaSCQRSService[ ApiKeyContext, ProductCommand, Product, ProductRejection, ProductNotification ](mkRejection): ``` -------------------------------- ### Define Initial State for CQRS Model Source: https://github.com/beyond-scale-group/edomata/blob/main/docs/tutorials/cqrs.md Define the initial state for a CQRS model using `CQRSModel`. This ensures model consistency and allows for easier evolution of the domain model. ```scala object Order extends CQRSModel[Order, Rejection] { def initial = Empty } ``` -------------------------------- ### Use for-comprehension with EitherNec Source: https://github.com/beyond-scale-group/edomata/blob/main/docs/tutorials/cqrs.md Demonstrates using a for-comprehension to chain operations on EitherNec values, where the entire sequence succeeds only if all individual steps result in a Right value. ```scala val e6 = for { a <- e4 b <- e5 } yield a + b ``` -------------------------------- ### Assert on Stomaton Scenario Results Source: https://github.com/beyond-scale-group/edomata/blob/main/docs/tutorials/cqrs.md Inspect the results of a Stomaton scenario run, checking the new state and any published notifications. This is crucial for verifying the behavior of the service. ```scala scenario1.result scenario1.notifications ``` -------------------------------- ### Composing Decisions Source: https://github.com/beyond-scale-group/edomata/blob/main/docs/tutorials/eventsourcing.md Illustrates how Decisions can be composed using mapping and sequencing operators. The `>>` operator sequences two Decisions, failing if either side rejects. ```scala val d4 = d1.map(_ * 2) val d5 = d2 >> d1 val d6 = d5 >> d3 ``` -------------------------------- ### Assert Edomaton results Source: https://github.com/beyond-scale-group/edomata/blob/main/docs/tutorials/eventsourcing.md Uses a test framework to verify the outcome of an Edomaton execution. ```scala assertEquals( obtained, EdomatonResult.Accepted( newState = Account.Open(0), events = ... notifications = ... ) ) ``` -------------------------------- ### Decision For-Comprehension (Expanded) Source: https://github.com/beyond-scale-group/edomata/blob/main/docs/tutorials/eventsourcing.md Provides the equivalent flatMap-based expansion of the for-comprehension for Decision chaining, illustrating how sequential operations are handled. ```scala Decision.pure(1).flatMap { i => Decision.accept("A").flatMap { _ => Decision.accept("B", "C").flatMap { _ => // ... and so on } } } ``` -------------------------------- ### Consume Outbox Items with OutboxConsumer Source: https://github.com/beyond-scale-group/edomata/blob/main/docs/tutorials/processes.md Uses OutboxConsumer to process items from the outbox. The provided action is executed for each item, and all items are marked as read upon successful completion. Failures in the action are not handled by OutboxConsumer. ```scala def publisher : Stream[IO, Nothing] = OutboxConsumer(backend){ item => // use outboxed item // e.g., send to Kafka, call external API, send email ??? } ``` -------------------------------- ### Generate migration SQL Source: https://github.com/beyond-scale-group/edomata/blob/main/docs/backends/doobie.md Generate DDL statements for event sourcing or CQRS to use with migration tools like Flyway. ```scala import edomata.backend.{PGNaming, PGSchema} // For event sourcing val ddl = PGSchema.eventsourcing( PGNaming.prefixed("accounts"), eventType = "jsonb", notificationType = "jsonb", snapshotType = "jsonb" ) ddl.foreach(println) // For CQRS val cqrsDdl = PGSchema.cqrs( PGNaming.prefixed("accounts"), stateType = "jsonb", notificationType = "jsonb" ) ``` -------------------------------- ### Configure RoleBasedPolicy Source: https://github.com/beyond-scale-group/edomata/blob/main/docs/tutorials/saas.md Create an AuthPolicy for CallerIdentity by mapping CRUD actions to required roles. ```scala given AuthPolicy[CallerIdentity] = RoleBasedPolicy { case CrudAction.Create => Set("write") case CrudAction.Read => Set("read") case CrudAction.Update => Set("write") case CrudAction.Delete => Set("admin") } ``` -------------------------------- ### Decision For-Comprehension Source: https://github.com/beyond-scale-group/edomata/blob/main/docs/tutorials/eventsourcing.md Demonstrates using Scala's for-comprehension to chain multiple Decision operations, including accepting single or multiple events and returning values. ```scala val d7 = for { i <- Decision.pure(1) _ <- Decision.accept("A") // accepting one event _ <- Decision.accept("B", "C") // accepting several events j <- Decision.acceptReturn(i * 2)("D", "E") // accepting several events and returning a value } yield i + j ``` -------------------------------- ### Define Commands and Notifications with Scala Enums Source: https://github.com/beyond-scale-group/edomata/blob/main/docs/tutorials/cqrs.md Define commands as imperative actions and notifications as facts using Scala enums. These are used to model the interactions and events within a service. ```scala 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 } ``` -------------------------------- ### Implement Custom AuthPolicy with Scopes Source: https://github.com/beyond-scale-group/edomata/blob/main/docs/tutorials/saas.md Provides a custom authentication policy for `ApiKeyContext` that authorizes actions based on required scopes. Ensures that only authorized actions are permitted. ```scala given AuthPolicy[ApiKeyContext] with def tenantId(auth: ApiKeyContext): TenantId = auth.tenantId def authorize(auth: ApiKeyContext, action: CrudAction): Either[String, Unit] = val required = action match case CrudAction.Create | CrudAction.Update => "catalog:write" case CrudAction.Read => "catalog:read" case CrudAction.Delete => "catalog:admin" if auth.scopes(required) then Right(()) else Left(s"Missing scope: $required") ``` -------------------------------- ### Build Daily Sales Report Projection Source: https://github.com/beyond-scale-group/edomata/blob/main/docs/tutorials/processes.md This Scala code defines a stream projection to build a daily sales report by processing journal events. It handles 'Deposited' and 'Withdrawn' events to record transactions. Ensure the 'backend.journal' and 'salesReport' are properly initialized. ```scala // Example: Build a daily sales report def salesReportProjection: Stream[IO, Unit] = backend.journal.readAllAfter(checkpoint).evalMap { journalEntry => journalEntry.event match { case Event.Deposited(amount) => salesReport.recordDeposit(journalEntry.time, amount) case Event.Withdrawn(amount) => salesReport.recordWithdrawal(journalEntry.time, amount) case _ => IO.unit } } ``` -------------------------------- ### Test Edge Case: Cannot Place Order Twice Source: https://github.com/beyond-scale-group/edomata/blob/main/docs/tutorials/cqrs.md Test an edge case where an order is already placed, demonstrating how the domain logic prevents placing a second order and returns the appropriate rejection. ```scala // Test: Can't place order twice Order.Placed("pizza", "office").place("burger", "home") // Returns: Left(NonEmptyChain(ExistingOrder)) ``` -------------------------------- ### Define Command and Notification ADTs Source: https://github.com/beyond-scale-group/edomata/blob/main/docs/tutorials/eventsourcing.md Define the domain-specific commands and integration notifications required for the service. ```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) } ``` -------------------------------- ### Perform Tenant-Scoped Queries Source: https://github.com/beyond-scale-group/edomata/blob/main/docs/tutorials/saas.md Use TenantScopedQuery to enforce tenant filtering automatically based on the provided auth context. ```scala import edomata.saas.* val listTodos: TenantScopedQuery[IO, CallerIdentity, Todo, Unit] = TenantScopedQuery[IO, CallerIdentity, Todo, Unit] { (tenantId, _) => // Your SQL always includes WHERE tenant_id = ? sql"SELECT data FROM todos WHERE tenant_id = $tenantId" .query[Todo].to[List].transact(xa) } // Usage: tenant filtering is automatic via AuthPolicy.tenantId listTodos.query(callerIdentity, ()) ``` -------------------------------- ### Generate Event Sourcing DDL with PGSchema Source: https://github.com/beyond-scale-group/edomata/blob/main/docs/backends/skunk.md Generate DDL statements for event sourcing tables using PGSchema.eventsourcing, specifying naming, event, notification, and snapshot types. ```scala import edomata.backend.{PGNaming, PGSchema} // For event sourcing val ddl = PGSchema.eventsourcing( PGNaming.prefixed("accounts"), eventType = "jsonb", notificationType = "jsonb", snapshotType = "jsonb" ) ddl.foreach(println) ```