Author SHA1 Message Date
flecomte 774e80d9d5 feature: init kotlin compose multiplatforme
Tests / build (pull_request) Successful in 7m44s
Tests / test (pull_request) Failing after 10m37s
Tests / lint (pull_request) Successful in 14m30s
2026-08-06 21:28:57 +02:00
flecomte 505cfe38f0 chore: add CLAUDE.md
Tests / build (push) Successful in 7m19s
Tests / test (push) Failing after 10m44s
Tests / lint (push) Successful in 13m49s
2026-08-06 19:52:48 +02:00
flecomte 85bcdcadb8 chore: remove unused redis 2026-08-06 19:42:32 +02:00
flecomte e67c475c38 chore: auto test new context in HexagonalArchitectureTest 2026-08-06 19:33:38 +02:00
flecomte 07983dc5b0 fix: fix HexagonalArchitectureTest 2026-08-06 19:23:47 +02:00
flecomte 5e9587b93f docs: clean 2026-08-06 19:10:32 +02:00
flecomte fd62f2574d docs: update openapi 2026-08-05 00:43:12 +02:00
flecomte 828bd0639e docs: clean docs 2026-08-05 00:19:03 +02:00
flecomte da13b0d2b8 chore: update docker compose in CI/CD
Tests / build (push) Successful in 6m45s
Tests / test (push) Failing after 11m30s
Tests / lint (push) Successful in 14m32s
2026-08-05 00:06:23 +02:00
flecomte d632dc0f6b chore: refactor docker compose env's
Tests / build (push) Successful in 6m41s
Tests / test (push) Failing after 10m1s
Tests / lint (push) Successful in 13m29s
2026-08-04 23:38:01 +02:00
flecomte b60e4aa457 chore: run CI test in docker
Tests / build (push) Successful in 7m13s
Tests / test (push) Failing after 9m19s
Tests / lint (push) Successful in 13m33s
2026-08-04 20:28:49 +02:00
flecomte e262f35b27 chore: eol=lf 2026-08-01 22:13:15 +02:00
flecomte 77e4cab8ad chore: split docker-compose-tools-local 2026-07-31 22:09:46 +02:00
flecomte 7291419c9f chore: fix postgresql.secret in ci
Tests / build (push) Successful in 6m17s
Tests / test (push) Failing after 11m40s
Tests / lint (push) Successful in 14m20s
2026-07-31 21:51:27 +02:00
224 changed files with 2631 additions and 1549 deletions
+26
View File
@@ -0,0 +1,26 @@
# Version control
.git
.github
# Gradle build outputs / caches (must always be rebuilt fresh inside the image)
.gradle
build/
.kotlin
!gradle/wrapper/gradle-wrapper.jar
# IDE
.idea
.vscode
.run
*.iml
*.iws
*.ipr
# Docker-only local files (secrets/env must never be baked into the image)
docker/.env
docker/*.env.docker
docker/*.secret
# Misc
*.hprof
.gradle-docker-cache/
+7
View File
@@ -0,0 +1,7 @@
* text=auto
* eol=lf
*.sh text eol=lf
*.png binary
*.jar binary
gradlew.bat eol=crlf
gradlew text eol=lf
+38 -22
View File
@@ -70,58 +70,74 @@ jobs:
run: chmod +x gradlew run: chmod +x gradlew
- name: Run lint - name: Run lint
run: ./gradlew ktlintCheck # Path scoped to :backend on purpose: :composeApp applies the Android Gradle plugin,
# which needs an Android SDK to even configure. Keeping every gradlew invocation on a
# fully-qualified project path (with org.gradle.configureondemand=true) lets CI skip
# configuring :composeApp entirely, so no Android SDK setup is needed on this runner.
run: ./gradlew :backend:ktlintCheck
- name: Publish ktlint report - name: Publish ktlint report
uses: yutailang0119/action-ktlint@v5 uses: yutailang0119/action-ktlint@v5
if: always() if: always()
with: with:
report-path: build/reports/ktlint/**/*.xml report-path: backend/build/reports/ktlint/**/*.xml
continue-on-error: false continue-on-error: false
test: test:
needs: build
runs-on: ubuntu-latest runs-on: ubuntu-latest
env:
GRADLE_CACHE_DIR: ${{ github.workspace }}/.gradle-docker-cache
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v6 uses: actions/checkout@v6
- name: Set up JDK 21 - name: Install a pinned Docker Compose version
uses: actions/setup-java@v5 run: |
with: mkdir -p ~/.docker/cli-plugins
distribution: 'temurin' curl -fSL https://github.com/docker/compose/releases/download/v5.1.4/docker-compose-linux-x86_64 \
java-version: '21' -o ~/.docker/cli-plugins/docker-compose
chmod +x ~/.docker/cli-plugins/docker-compose
docker compose version
- name: Restore Gradle cache - name: Prepare docker secrets
run: |
[ -f docker/postgresql.secret ] || echo -n "changeit" > docker/postgresql.secret
- name: Generate cache key
id: cache-key-generator
run: echo "key=gradle-docker-${{ runner.os }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}" >> $GITHUB_OUTPUT
- name: Restore Gradle cache (Docker)
uses: actions/cache@v6 uses: actions/cache@v6
with: with:
path: | path: ${{ env.GRADLE_CACHE_DIR }}
~/.gradle/caches key: ${{ steps.cache-key-generator.outputs.key }}
~/.gradle/wrapper
key: ${{ needs.build.outputs.cache-key }}
restore-keys: | restore-keys: |
gradle-${{ runner.os }}- gradle-docker-${{ runner.os }}-
- name: Grant execute permission to Gradle wrapper - name: Prepare cache directory permissions
run: chmod +x gradlew run: |
mkdir -p "$GRADLE_CACHE_DIR"
chmod -R 777 "$GRADLE_CACHE_DIR"
- name: Start CI Docker Compose services - name: Run tests in Docker
run: ./gradlew ciComposeUp -Pci run: docker compose -f docker/docker-compose-test.yaml run tests
- name: Run tests - name: Shut down Docker services
run: ./gradlew test -x ciComposeUp -Pci --no-daemon if: always()
run: docker compose -f docker/docker-compose-test.yaml down -v
- name: Upload test reports - name: Upload test reports
if: always() if: always()
uses: actions/upload-artifact@v7 uses: actions/upload-artifact@v7
with: with:
name: test-results name: test-results
path: build/reports/tests/test path: backend/build/reports/tests/test
- name: Publish Test Report - name: Publish Test Report
uses: dorny/test-reporter@v3 uses: dorny/test-reporter@v3
if: always() if: always()
with: with:
name: JUnit Tests name: JUnit Tests
path: build/test-results/test/TEST-*.xml path: backend/build/test-results/test/TEST-*.xml
reporter: java-junit reporter: java-junit
+7
View File
@@ -1,9 +1,15 @@
.gradle .gradle
.kotlin/
build/ build/
!gradle/wrapper/gradle-wrapper.jar !gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/ !**/src/main/**/build/
!**/src/test/**/build/ !**/src/test/**/build/
### Android / KMP ###
local.properties
captures/
.cxx/
### STS ### ### STS ###
.apt_generated .apt_generated
.classpath .classpath
@@ -37,3 +43,4 @@ out/
/docker/.env /docker/.env
/docker/*.secret /docker/*.secret
*.hprof *.hprof
/.gradle-docker-cache/
+133
View File
@@ -0,0 +1,133 @@
# CLAUDE.md — event-demo
Ce fichier donne le contexte du projet pour toute session Claude Code future sur ce dépôt.
## Vue d'ensemble
`event-demo` est un projet démo personnel (Fabrice Lecomte) qui illustre plusieurs patterns
d'architecture backend :
- Event Sourcing
- Event-Driven (bus d'événements asynchrone)
- CQRS (séparation commandes / projections en lecture)
- Architecture Hexagonale (ports & adapters), un dossier par *bounded context*
Le cas d'usage servant de support est un jeu de cartes façon UNO (créer une partie, rejoindre,
jouer une carte, piocher, etc.), avec authentification des joueurs.
Dépôts distants configurés : `gitea` (auto-hébergé, git.gogn.synology.me — remote historique)
et `github` (`flecomte/event-demo`, miroir). Vérifier vers lequel pousser selon le contexte.
## Stack technique
- **Langage** : Kotlin 2.1.21, JDK 21 (toolchain Gradle)
- **Framework serveur** : Ktor 3.5.1 (Netty), DI via Koin 4.2.1
- **Sérialisation** : kotlinx.serialization (JSON)
- **Persistance** :
- PostgreSQL (event store, via HikariCP) + migrations Flyway (`migrations/events/`)
- RabbitMQ (bus d'événements / bus de commandes, via amqp-client)
- **Auth** : JWT (ktor-server-auth-jwt), hash de mot de passe via password4j
- **Infra dev/prod** : Docker Compose (fichiers `docker/docker-compose-{dev,test,prod}.yaml`
incluant des « parts » réutilisables dans `docker/parts/`), reverse proxy Træfik
- **Tests** : Kotest (runner JUnit5), MockK, kotest-extensions-koin, ArchUnit (test d'architecture)
- **Qualité** : ktlint (`ktlint_official`, standard + experimental activés), reporting checkstyle
- **CI** : GitHub Actions (`.github/workflows/tests.yml`) — build/cache Gradle, `ktlintCheck`,
puis tests exécutés **dans Docker** (`docker compose -f docker/docker-compose-test.yaml run tests`)
- **API** : documentée en OpenAPI (`resources/openapi/documentation.yaml`)
## Architecture
Un dossier par *bounded context* sous `src/main/kotlin/eventDemo/contexts/<context>/`, chacun
strictement découpé en 3 couches :
- `domain/` — aucune dépendance vers les autres couches
- `application/` — ne dépend que de `domain`
- `infrastructure/` — dépend de `domain` et `application`
Contexts actuels :
- **`auth`** : `User`, création de compte, login JWT, event store dédié (Postgresql),
projection utilisateur.
- **`game`** : cœur du jeu — `Card`, `DrawPile`/`DiscardPile`, `Player`, `GameId`, commandes
(`JoinTheGameCommand`, `PlayCardCommand`, `ReadyToPlayCommand`, `TakeCartFromDrawPileCommand`),
state machine du jeu via `sealed interface Game` (`GameInit``GameCreated``GameStarted`
`GameEnded`), notifications, projections (liste de parties), listeners/réactions.
Libs transverses dans `libs/` (indépendantes de tout contexte) :
- `bus/` — abstraction `Bus<E>` avec implémentations in-memory et RabbitMQ (fanout exchange)
- `command/``Command`, `CommandUnicityChecker` (empêche la double exécution d'une commande,
cache glissant de 10 min par défaut)
- `eventSource/``Event`, `EventStream` (append/lecture par version, gestion de
`VersionConflictException`), `EventStore` in-memory / Postgresql
- `helpers/`, `serializer/` — utilitaires (conversion de frames WebSocket, sérialiseurs UUID, etc.)
## Patterns notables dans le code
- **Event sourcing** : `Game.loadFromHistory(events)` reconstruit l'état en repliant
(`fold`) les événements sur une state machine scellée, en utilisant la syntaxe Kotlin 2.1
`when` avec garde `if` (ex. `is GameCreatedEvent if this is GameInit -> applyEvent(event)`).
- **CQRS** : écriture via les command handlers (`application/command/handlers`), lecture via des
projections dédiées (`application/projections`), propagées via le bus RabbitMQ, pas de couplage
direct avec l'écriture.
- **Event-driven** : réactions asynchrones (`ReactionListener`, `EventToNotificationSubscriber`)
déclenchées par le bus RabbitMQ (exchange fanout, une queue par abonné).
- **Exceptions métier** : hiérarchie `GameException` / `IllegalActionException` dans
`domain/game/errors`, une exception par règle métier violée (ex.
`NeedMorePlayersToStartGameException`, `ItsNotTheTurnException`).
## Commandes utiles
```shell
./gradlew build # build complet
./gradlew test # tests (JUnit5 via Kotest)
./gradlew ktlintCheck # lint
./gradlew ktlintFormat # auto-format
./gradlew buildFatJar # jar exécutable "all-in-one" (utilisé par le Dockerfile prod)
# Dépendances seules (Postgres, RabbitMQ, Træfik, pgAdmin...) pour lancer l'app en local hors docker
docker compose -f docker/docker-compose-dev.yaml up -d
# Stack de test façon CI
docker compose -f docker/docker-compose-test.yaml up -d
# ou directement (comme en CI) :
docker compose -f docker/docker-compose-test.yaml run tests
# Stack complète en prod
docker compose -f docker/docker-compose-prod.yaml -p event-demo up -d
```
URLs en dev (voir `doc/installation.md`, nécessite Træfik + résolution des `*.traefik.me`) :
API sur `http://api.traefik.me/`, dashboard
Træfik, pgAdmin et RabbitMQ management exposés via des sous-domaines `traefik.me`.
## Conventions de code
- ktlint en mode `ktlint_official` + règles `standard` et `experimental` activées
(voir `.editorconfig`), indentation **2 espaces**, virgules finales (*trailing commas*)
systématiques, wrapping forcé des expressions/signatures multi-lignes.
- Fins de ligne forcées en **LF** (`.gitattributes`), sauf `gradlew.bat` en CRLF.
- Code et identifiants en anglais.
- Style Kotlin idiomatique/fonctionnel : `fold`, `let`, `apply`, `when` exhaustifs, classes/interfaces
scellées (`sealed class`/`sealed interface`) pour modéliser états et événements plutôt que des enums
avec des champs optionnels.
## Pièges connus / choses à savoir avant de toucher au build ou à la CI
- **MockK/ByteBuddy en Docker** : l'auto-attach dynamique de MockK échoue dans les conteneurs
(le handshake SIGQUIT de l'AttachListener JVM time-out). Le `build.gradle.kts` charge donc
l'agent `byte-buddy-agent` de façon statique via `-javaagent` pour les tâches `Test`, afin
que MockK détecte l'instrumentation déjà présente et saute l'attach dynamique. Ne pas retirer
ce bloc sans repenser l'exécution des tests en Docker.
- **Secret Postgres en CI** : `docker/postgresql.secret` est généré à la volée par le workflow
GitHub Actions s'il n'existe pas (`echo -n "changeit" > docker/postgresql.secret`) — normal,
pas un fichier à committer.
- Les tests « officiels » de la CI tournent **dans Docker**, pas directement via `./gradlew test`
sur l'hôte — en cas de comportement différent entre local et CI, vérifier d'abord les
variables d'environnement/versions du `docker-compose-test.yaml`.
## Historique récent (pour contexte)
Le projet a connu un « Massive refactor to build the V2 » (commit `e2d7942`) : passage d'une
architecture par couches techniques plates (`adapter/presenter/domain`) à l'organisation actuelle
par bounded context (`auth`/`game`) avec 3 couches hexagonales chacune.
+1 -27
View File
@@ -2,7 +2,6 @@ Event Demo
========== ==========
- [Installation](./doc/installation.md) - [Installation](./doc/installation.md)
- [What's the demo for ?](#whats-the-demo-for-) - [What's the demo for ?](#whats-the-demo-for-)
- [What's in this demo](#whats-in-this-demo)
- [The stack](#the-stack) - [The stack](#the-stack)
- [Architecture](./doc/architecture.md) - [Architecture](./doc/architecture.md)
@@ -18,31 +17,6 @@ of different patterns and architectures.
- The CQRS pattern. - The CQRS pattern.
- With the Hexagonal architecture. - With the Hexagonal architecture.
What's in this demo
-------------------
- The **event sourcing** pattern.
- The **event driven** pattern.
- The **CQRS** pattern with **command** and **query**.
- A fully **asynchronous** architecture.Concurently process.
- A **pure Kotlin** implementation of **readmodel**/**projection**.
- A **Redis** implementation of **readmodel**/**projection**.
- A **pure Kotlin** implementation of **Event Store**.
- A **Postgresql** implementation of **Event Store**.
- A **pure Kotlin** implementation of **Event Bus**.
- A **RabbitMQ** implementation of **Event Bus**.
- A **Hexagonal** architecture.
- Use of **Web Sockets**.
- Use of the classic **Rest** route.
- Simple usage of the **JWT**.
- The **Ktor** framework.
- The **Koin** Dependency Injection framework
- Concurrently process.
- Use of coroutines.
- Using **docker compose** for the stack with **traefik**.
- Use of **flyway** to migrate the postgresql schema.
The stack The stack
--------- ---------
@@ -51,11 +25,11 @@ Language
Framework Framework
- Ktor - Ktor
- with Koin for Dependency Injection
Database Database
- Postgresql - Postgresql
- with Flyway - with Flyway
- Redis
- RabbitMQ - RabbitMQ
Infra Infra
+105
View File
@@ -0,0 +1,105 @@
import org.jlleitschuh.gradle.ktlint.KtlintExtension
val ktorVersion: Provider<String> = providers.gradleProperty("ktor_version")
val kotlinVersion: Provider<String> = providers.gradleProperty("kotlin_version")
val kotlinSerializationVersion: Provider<String> = providers.gradleProperty("kotlin_serialization_version")
val logbackVersion: Provider<String> = providers.gradleProperty("logback_version")
val koinVersion: Provider<String> = providers.gradleProperty("koin_version")
val kotlinLoggingVersion: Provider<String> = providers.gradleProperty("kotlin_logging_version")
val kotestVersion: Provider<String> = providers.gradleProperty("kotest_version")
plugins {
application
kotlin("jvm")
id("io.ktor.plugin") version "3.5.1"
id("org.jetbrains.kotlin.plugin.serialization")
id("org.jlleitschuh.gradle.ktlint") version "14.2.0"
}
group = "io.github.flecomte"
application {
mainClass.set("eventDemo.ApplicationKt")
val isDevelopment: Boolean = project.ext.has("development")
applicationDefaultJvmArgs = listOf("-Dio.ktor.development=$isDevelopment")
}
configure<KtlintExtension> {
version.set("1.8.0")
}
ktlint {
reporters {
reporter(org.jlleitschuh.gradle.ktlint.reporter.ReporterType.CHECKSTYLE)
}
}
repositories {
mavenCentral()
}
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
kotlin {
compilerOptions {
freeCompilerArgs.add("-opt-in=kotlin.uuid.ExperimentalUuidApi")
}
}
tasks.withType<Test>().configureEach {
useJUnitPlatform()
jvmArgs("-Djdk.attach.allowAttachSelf=true", "-XX:+EnableDynamicAgentLoading")
// Dynamic self-attach (used by MockK/ByteBuddy) times out in Docker containers because the
// SIGQUIT-triggered AttachListener handshake never completes there. Loading the byte-buddy
// agent jar statically via -javaagent avoids the attach handshake entirely: MockK detects the
// pre-installed Instrumentation instance and skips dynamic attach.
doFirst {
val agentJar =
classpath.files.firstOrNull { it.name.startsWith("byte-buddy-agent") }
?: error("byte-buddy-agent jar not found on test classpath")
jvmArgs("-javaagent:$agentJar")
}
}
dependencies {
implementation(project(":shared"))
implementation("io.ktor:ktor-server-core-jvm")
implementation("io.ktor:ktor-server-auth-jvm")
implementation("io.ktor:ktor-server-auth-jwt-jvm")
implementation("io.ktor:ktor-server-auto-head-response-jvm")
implementation("io.ktor:ktor-server-resources")
implementation("io.ktor:ktor-server-content-negotiation-jvm")
implementation("io.ktor:ktor-serialization-kotlinx-json-jvm")
implementation("io.ktor:ktor-server-websockets-jvm")
implementation("io.ktor:ktor-server-cors-jvm")
implementation("io.ktor:ktor-server-host-common-jvm")
implementation("io.ktor:ktor-server-status-pages-jvm")
implementation("io.ktor:ktor-server-netty-jvm")
implementation("io.ktor:ktor-server-data-conversion")
implementation("io.ktor:ktor-client-content-negotiation")
implementation("io.ktor:ktor-client-auth")
implementation("ch.qos.logback:logback-classic:${logbackVersion.get()}")
implementation("io.insert-koin:koin-ktor:${koinVersion.get()}")
implementation("io.insert-koin:koin-logger-slf4j:${koinVersion.get()}")
implementation("io.github.oshai:kotlin-logging-jvm:${kotlinLoggingVersion.get()}")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:${kotlinSerializationVersion.get()}")
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.6.2")
implementation("org.postgresql:postgresql:42.7.13")
implementation("com.zaxxer:HikariCP:6.3.0")
implementation("com.rabbitmq:amqp-client:5.25.0")
implementation("com.password4j:password4j:1.8.4")
// Force version of sub library (for security)
implementation("commons-codec:commons-codec:1.13")
testImplementation("io.kotest:kotest-extensions-koin:${kotestVersion.get()}")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit:${kotlinVersion.get()}")
testImplementation("io.ktor:ktor-server-test-host-jvm:${ktorVersion.get()}")
testImplementation("io.kotest:kotest-runner-junit5:${kotestVersion.get()}")
testImplementation("io.mockk:mockk:1.14.11")
testImplementation("com.tngtech.archunit:archunit-junit5:1.3.0")
}
@@ -3,7 +3,6 @@ package eventDemo.configuration
import io.ktor.server.config.ApplicationConfig import io.ktor.server.config.ApplicationConfig
data class Configuration( data class Configuration(
val redisUrl: String,
val jwtSecret: String, val jwtSecret: String,
val postgresql: Postgresql, val postgresql: Postgresql,
val rabbitmq: RabbitMQ, val rabbitmq: RabbitMQ,
@@ -25,7 +24,6 @@ data class Configuration(
val ApplicationConfig.configuration val ApplicationConfig.configuration
get() = get() =
Configuration( Configuration(
redisUrl = getProperty("redis.url"),
jwtSecret = getProperty("jwt.secret"), jwtSecret = getProperty("jwt.secret"),
postgresql = postgresql =
Configuration.Postgresql( Configuration.Postgresql(
@@ -7,8 +7,6 @@ import org.koin.core.module.Module
import org.koin.core.scope.Scope import org.koin.core.scope.Scope
import org.koin.core.scope.ScopeCallback import org.koin.core.scope.ScopeCallback
import org.koin.dsl.bind import org.koin.dsl.bind
import redis.clients.jedis.JedisPooled
import redis.clients.jedis.UnifiedJedis
import javax.sql.DataSource import javax.sql.DataSource
fun Module.configureDIDataSource(config: Configuration) { fun Module.configureDIDataSource(config: Configuration) {
@@ -26,11 +24,6 @@ fun Module.configureDIDataSource(config: Configuration) {
} }
} bind DataSource::class } bind DataSource::class
// Redis (for Projections)
single {
JedisPooled(config.redisUrl)
} bind UnifiedJedis::class
// RabbitMQ (for EventBus) // RabbitMQ (for EventBus)
factory { factory {
ConnectionFactory().apply { ConnectionFactory().apply {
@@ -2,7 +2,7 @@ package eventDemo.contexts.auth.application.eventStores
import eventDemo.contexts.auth.application.ports.UserEventStore import eventDemo.contexts.auth.application.ports.UserEventStore
import eventDemo.contexts.auth.domain.User import eventDemo.contexts.auth.domain.User
import eventDemo.sharedKernel.UserId import eventDemo.shared.ids.UserId
class UserEventStoreRepository( class UserEventStoreRepository(
val eventStore: UserEventStore, val eventStore: UserEventStore,
@@ -1,7 +1,7 @@
package eventDemo.contexts.auth.application.eventStores package eventDemo.contexts.auth.application.eventStores
import eventDemo.contexts.auth.domain.User import eventDemo.contexts.auth.domain.User
import eventDemo.sharedKernel.UserId import eventDemo.shared.ids.UserId
interface UserRepository { interface UserRepository {
fun get(id: UserId): User? fun get(id: UserId): User?
@@ -2,6 +2,6 @@ package eventDemo.contexts.auth.application.ports
import eventDemo.contexts.auth.domain.events.UserEvent import eventDemo.contexts.auth.domain.events.UserEvent
import eventDemo.libs.eventSource.eventStore.EventStore import eventDemo.libs.eventSource.eventStore.EventStore
import eventDemo.sharedKernel.UserId import eventDemo.shared.ids.UserId
interface UserEventStore : EventStore<UserEvent, UserId> interface UserEventStore : EventStore<UserEvent, UserId>
@@ -2,7 +2,7 @@ package eventDemo.contexts.auth.domain
import eventDemo.contexts.auth.domain.events.NewUserCreatedEvent import eventDemo.contexts.auth.domain.events.NewUserCreatedEvent
import eventDemo.contexts.auth.domain.events.UserEvent import eventDemo.contexts.auth.domain.events.UserEvent
import eventDemo.sharedKernel.UserId import eventDemo.shared.ids.UserId
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
@@ -1,7 +1,7 @@
package eventDemo.contexts.auth.domain.events package eventDemo.contexts.auth.domain.events
import eventDemo.libs.eventSource.EventId import eventDemo.shared.ids.EventId
import eventDemo.sharedKernel.UserId import eventDemo.shared.ids.UserId
import kotlinx.datetime.Clock import kotlinx.datetime.Clock
import kotlinx.datetime.Instant import kotlinx.datetime.Instant
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@@ -1,7 +1,7 @@
package eventDemo.contexts.auth.domain.events package eventDemo.contexts.auth.domain.events
import eventDemo.libs.eventSource.Event import eventDemo.libs.eventSource.Event
import eventDemo.sharedKernel.UserId import eventDemo.shared.ids.UserId
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
@@ -5,7 +5,7 @@ import com.auth0.jwt.algorithms.Algorithm
import eventDemo.configuration.configuration import eventDemo.configuration.configuration
import eventDemo.contexts.auth.domain.User import eventDemo.contexts.auth.domain.User
import eventDemo.contexts.auth.infrastructure.persistence.projection.UserProjection import eventDemo.contexts.auth.infrastructure.persistence.projection.UserProjection
import eventDemo.sharedKernel.UserId import eventDemo.shared.ids.UserId
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
import io.ktor.server.application.Application import io.ktor.server.application.Application
import io.ktor.server.auth.authentication import io.ktor.server.auth.authentication
@@ -4,7 +4,7 @@ import eventDemo.contexts.auth.application.ports.UserEventStore
import eventDemo.contexts.auth.domain.events.UserEvent import eventDemo.contexts.auth.domain.events.UserEvent
import eventDemo.libs.eventSource.eventStore.EventStore import eventDemo.libs.eventSource.eventStore.EventStore
import eventDemo.libs.eventSource.eventStore.EventStoreInMemory import eventDemo.libs.eventSource.eventStore.EventStoreInMemory
import eventDemo.sharedKernel.UserId import eventDemo.shared.ids.UserId
/** /**
* A stream to publish and read the user events. * A stream to publish and read the user events.
@@ -4,7 +4,7 @@ import eventDemo.contexts.auth.application.ports.UserEventStore
import eventDemo.contexts.auth.domain.events.UserEvent import eventDemo.contexts.auth.domain.events.UserEvent
import eventDemo.libs.eventSource.eventStore.EventStore import eventDemo.libs.eventSource.eventStore.EventStore
import eventDemo.libs.eventSource.eventStore.EventStoreInPostgresql import eventDemo.libs.eventSource.eventStore.EventStoreInPostgresql
import eventDemo.sharedKernel.UserId import eventDemo.shared.ids.UserId
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import javax.sql.DataSource import javax.sql.DataSource
@@ -1,6 +1,6 @@
package eventDemo.contexts.auth.infrastructure.persistence.projection package eventDemo.contexts.auth.infrastructure.persistence.projection
import eventDemo.sharedKernel.UserId import eventDemo.shared.ids.UserId
data class UserProjection( data class UserProjection(
val id: UserId, val id: UserId,
@@ -3,9 +3,10 @@ package eventDemo.contexts.auth.infrastructure.persistence.projection
import eventDemo.contexts.auth.application.ports.UserProjectionRepository import eventDemo.contexts.auth.application.ports.UserProjectionRepository
import eventDemo.contexts.auth.infrastructure.checkPassword import eventDemo.contexts.auth.infrastructure.checkPassword
import eventDemo.contexts.auth.infrastructure.hashPassword import eventDemo.contexts.auth.infrastructure.hashPassword
import eventDemo.sharedKernel.UserId import eventDemo.shared.ids.UserId
import java.util.UUID
import javax.sql.DataSource import javax.sql.DataSource
import kotlin.uuid.Uuid
import kotlin.uuid.toJavaUuid
class UserProjectionRepositoryInPostgresql( class UserProjectionRepositoryInPostgresql(
val dataSource: DataSource, val dataSource: DataSource,
@@ -24,7 +25,7 @@ class UserProjectionRepositoryInPostgresql(
}.use { resultSet -> }.use { resultSet ->
if (resultSet.next()) { if (resultSet.next()) {
UserProjection( UserProjection(
id = UserId(UUID.fromString(resultSet.getString("id"))), id = UserId(Uuid.parse(resultSet.getString("id"))),
username = resultSet.getString("username"), username = resultSet.getString("username"),
password = resultSet.getString("password"), password = resultSet.getString("password"),
) )
@@ -42,7 +43,7 @@ class UserProjectionRepositoryInPostgresql(
values (?, ?) values (?, ?)
""".trimIndent(), """.trimIndent(),
).use { ).use {
it.setObject(1, user.id) it.setObject(1, user.id.id.toJavaUuid())
it.setString(2, user.username) it.setString(2, user.username)
it.executeUpdate() it.executeUpdate()
} }
@@ -1,11 +1,11 @@
package eventDemo.contexts.game.application.channels package eventDemo.contexts.game.application.channels
import eventDemo.contexts.game.application.command.models.GameCommand
import eventDemo.contexts.game.application.notification.CommandSubscriber import eventDemo.contexts.game.application.notification.CommandSubscriber
import eventDemo.contexts.game.application.notification.EventToNotificationSubscriber import eventDemo.contexts.game.application.notification.EventToNotificationSubscriber
import eventDemo.contexts.game.application.notification.models.Notification import eventDemo.shared.game.command.GameCommand
import eventDemo.contexts.game.domain.game.GameId import eventDemo.shared.game.notification.Notification
import eventDemo.sharedKernel.UserId import eventDemo.shared.ids.GameId
import eventDemo.shared.ids.UserId
import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.channels.ReceiveChannel import kotlinx.coroutines.channels.ReceiveChannel
import kotlinx.coroutines.channels.SendChannel import kotlinx.coroutines.channels.SendChannel
@@ -1,11 +1,11 @@
package eventDemo.contexts.game.application.command.handlers package eventDemo.contexts.game.application.command.handlers
import eventDemo.contexts.game.application.command.models.GameCommand import eventDemo.shared.game.command.GameCommand
import eventDemo.contexts.game.application.command.models.JoinTheGameCommand import eventDemo.shared.game.command.JoinTheGameCommand
import eventDemo.contexts.game.application.command.models.PlayCardCommand import eventDemo.shared.game.command.PlayCardCommand
import eventDemo.contexts.game.application.command.models.ReadyToPlayCommand import eventDemo.shared.game.command.ReadyToPlayCommand
import eventDemo.contexts.game.application.command.models.TakeCartFromDrawPileCommand import eventDemo.shared.game.command.TakeCartFromDrawPileCommand
import eventDemo.contexts.game.domain.game.GameId import eventDemo.shared.ids.GameId
import java.util.Collections import java.util.Collections
class GameCommandHandlerDispatcher( class GameCommandHandlerDispatcher(
@@ -1,12 +1,12 @@
package eventDemo.contexts.game.application.command.handlers package eventDemo.contexts.game.application.command.handlers
import eventDemo.contexts.game.application.command.models.GameCommand
import eventDemo.contexts.game.application.eventStores.GameRepository import eventDemo.contexts.game.application.eventStores.GameRepository
import eventDemo.contexts.game.application.ports.GameEventBus import eventDemo.contexts.game.application.ports.GameEventBus
import eventDemo.contexts.game.domain.events.GameEvent import eventDemo.contexts.game.domain.events.GameEvent
import eventDemo.contexts.game.domain.game.gameState.Game import eventDemo.contexts.game.domain.game.gameState.Game
import eventDemo.libs.command.Command
import eventDemo.libs.eventSource.eventStore.VersionConflictException import eventDemo.libs.eventSource.eventStore.VersionConflictException
import eventDemo.shared.command.Command
import eventDemo.shared.game.command.GameCommand
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import kotlin.reflect.KClass import kotlin.reflect.KClass
@@ -1,10 +1,10 @@
package eventDemo.contexts.game.application.command.handlers package eventDemo.contexts.game.application.command.handlers
import eventDemo.contexts.auth.application.eventStores.UserRepository import eventDemo.contexts.auth.application.eventStores.UserRepository
import eventDemo.contexts.game.application.command.models.JoinTheGameCommand
import eventDemo.contexts.game.application.eventStores.GameRepository import eventDemo.contexts.game.application.eventStores.GameRepository
import eventDemo.contexts.game.application.ports.GameEventBus import eventDemo.contexts.game.application.ports.GameEventBus
import eventDemo.contexts.game.domain.game.gameState.GameCreated import eventDemo.contexts.game.domain.game.gameState.GameCreated
import eventDemo.shared.game.command.JoinTheGameCommand
/** /**
* A command to perform an action to play a new card * A command to perform an action to play a new card
@@ -1,9 +1,9 @@
package eventDemo.contexts.game.application.command.handlers package eventDemo.contexts.game.application.command.handlers
import eventDemo.contexts.game.application.command.models.PlayCardCommand
import eventDemo.contexts.game.application.eventStores.GameRepository import eventDemo.contexts.game.application.eventStores.GameRepository
import eventDemo.contexts.game.application.ports.GameEventBus import eventDemo.contexts.game.application.ports.GameEventBus
import eventDemo.contexts.game.domain.game.gameState.GameStarted import eventDemo.contexts.game.domain.game.gameState.GameStarted
import eventDemo.shared.game.command.PlayCardCommand
/** /**
* A command to perform an action to play a new card * A command to perform an action to play a new card
@@ -1,9 +1,9 @@
package eventDemo.contexts.game.application.command.handlers package eventDemo.contexts.game.application.command.handlers
import eventDemo.contexts.game.application.command.models.ReadyToPlayCommand
import eventDemo.contexts.game.application.eventStores.GameRepository import eventDemo.contexts.game.application.eventStores.GameRepository
import eventDemo.contexts.game.application.ports.GameEventBus import eventDemo.contexts.game.application.ports.GameEventBus
import eventDemo.contexts.game.domain.game.gameState.GameCreated import eventDemo.contexts.game.domain.game.gameState.GameCreated
import eventDemo.shared.game.command.ReadyToPlayCommand
/** /**
* A command to set as ready to play * A command to set as ready to play
@@ -1,9 +1,9 @@
package eventDemo.contexts.game.application.command.handlers package eventDemo.contexts.game.application.command.handlers
import eventDemo.contexts.game.application.command.models.TakeCartFromDrawPileCommand
import eventDemo.contexts.game.application.eventStores.GameRepository import eventDemo.contexts.game.application.eventStores.GameRepository
import eventDemo.contexts.game.application.ports.GameEventBus import eventDemo.contexts.game.application.ports.GameEventBus
import eventDemo.contexts.game.domain.game.gameState.GameStarted import eventDemo.contexts.game.domain.game.gameState.GameStarted
import eventDemo.shared.game.command.TakeCartFromDrawPileCommand
/** /**
* A command to draw card on draw pile. * A command to draw card on draw pile.
@@ -1,9 +1,9 @@
package eventDemo.contexts.game.application.eventStores package eventDemo.contexts.game.application.eventStores
import eventDemo.contexts.game.application.ports.GameEventStore import eventDemo.contexts.game.application.ports.GameEventStore
import eventDemo.contexts.game.domain.game.GameId
import eventDemo.contexts.game.domain.game.gameState.Game import eventDemo.contexts.game.domain.game.gameState.Game
import eventDemo.libs.eventSource.eventStore.VersionConflictException import eventDemo.libs.eventSource.eventStore.VersionConflictException
import eventDemo.shared.ids.GameId
class GameEventStoreRepository( class GameEventStoreRepository(
val eventStore: GameEventStore, val eventStore: GameEventStore,
@@ -1,10 +1,10 @@
package eventDemo.contexts.game.application.eventStores package eventDemo.contexts.game.application.eventStores
import eventDemo.contexts.game.domain.game.GameId
import eventDemo.contexts.game.domain.game.gameState.Game import eventDemo.contexts.game.domain.game.gameState.Game
import eventDemo.contexts.game.domain.game.gameState.GameCreated import eventDemo.contexts.game.domain.game.gameState.GameCreated
import eventDemo.contexts.game.domain.game.gameState.GameInit import eventDemo.contexts.game.domain.game.gameState.GameInit
import eventDemo.libs.eventSource.eventStore.VersionConflictException import eventDemo.libs.eventSource.eventStore.VersionConflictException
import eventDemo.shared.ids.GameId
interface GameRepository { interface GameRepository {
fun get(id: GameId): Game? fun get(id: GameId): Game?
@@ -1,16 +1,5 @@
package eventDemo.contexts.game.application.notification package eventDemo.contexts.game.application.notification
import eventDemo.contexts.game.application.notification.models.ItsTheTurnOfNotification
import eventDemo.contexts.game.application.notification.models.Notification
import eventDemo.contexts.game.application.notification.models.PilesShuffledNotification
import eventDemo.contexts.game.application.notification.models.PlayerAsJoinTheGameNotification
import eventDemo.contexts.game.application.notification.models.PlayerAsPlayACardNotification
import eventDemo.contexts.game.application.notification.models.PlayerHavePassNotification
import eventDemo.contexts.game.application.notification.models.PlayerWasReadyNotification
import eventDemo.contexts.game.application.notification.models.PlayerWinNotification
import eventDemo.contexts.game.application.notification.models.TheGameWasStartedNotification
import eventDemo.contexts.game.application.notification.models.WelcomeToTheGameNotification
import eventDemo.contexts.game.application.notification.models.YourNewCardNotification
import eventDemo.contexts.game.domain.events.CardIsPlayedEvent import eventDemo.contexts.game.domain.events.CardIsPlayedEvent
import eventDemo.contexts.game.domain.events.DrawFilledWithDiscardEvent import eventDemo.contexts.game.domain.events.DrawFilledWithDiscardEvent
import eventDemo.contexts.game.domain.events.GameCreatedEvent import eventDemo.contexts.game.domain.events.GameCreatedEvent
@@ -23,7 +12,18 @@ import eventDemo.contexts.game.domain.events.PlayerReadyEvent
import eventDemo.contexts.game.domain.events.PlayerWinEvent import eventDemo.contexts.game.domain.events.PlayerWinEvent
import eventDemo.contexts.game.domain.game.gameState.Game import eventDemo.contexts.game.domain.game.gameState.Game
import eventDemo.contexts.game.domain.game.gameState.GameStarted import eventDemo.contexts.game.domain.game.gameState.GameStarted
import eventDemo.sharedKernel.UserId import eventDemo.shared.game.notification.ItsTheTurnOfNotification
import eventDemo.shared.game.notification.Notification
import eventDemo.shared.game.notification.PilesShuffledNotification
import eventDemo.shared.game.notification.PlayerAsJoinTheGameNotification
import eventDemo.shared.game.notification.PlayerAsPlayACardNotification
import eventDemo.shared.game.notification.PlayerHavePassNotification
import eventDemo.shared.game.notification.PlayerWasReadyNotification
import eventDemo.shared.game.notification.PlayerWinNotification
import eventDemo.shared.game.notification.TheGameWasStartedNotification
import eventDemo.shared.game.notification.WelcomeToTheGameNotification
import eventDemo.shared.game.notification.YourNewCardNotification
import eventDemo.shared.ids.UserId
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import io.github.oshai.kotlinlogging.withLoggingContext import io.github.oshai.kotlinlogging.withLoggingContext
@@ -1,7 +1,6 @@
package eventDemo.contexts.game.application.notification package eventDemo.contexts.game.application.notification
import eventDemo.contexts.game.application.command.handlers.GameCommandHandlerDispatcher import eventDemo.contexts.game.application.command.handlers.GameCommandHandlerDispatcher
import eventDemo.contexts.game.application.command.models.GameCommand
import eventDemo.contexts.game.application.eventStores.GameRepository import eventDemo.contexts.game.application.eventStores.GameRepository
import eventDemo.contexts.game.application.logging.LoggingContextKeys.Command import eventDemo.contexts.game.application.logging.LoggingContextKeys.Command
import eventDemo.contexts.game.application.logging.LoggingContextKeys.CurrentUserId import eventDemo.contexts.game.application.logging.LoggingContextKeys.CurrentUserId
@@ -9,12 +8,13 @@ import eventDemo.contexts.game.application.logging.LoggingContextKeys.Event
import eventDemo.contexts.game.application.logging.LoggingContextKeys.Game import eventDemo.contexts.game.application.logging.LoggingContextKeys.Game
import eventDemo.contexts.game.application.logging.LoggingContextKeys.Notification import eventDemo.contexts.game.application.logging.LoggingContextKeys.Notification
import eventDemo.contexts.game.application.logging.withLoggingContext import eventDemo.contexts.game.application.logging.withLoggingContext
import eventDemo.contexts.game.application.notification.models.Notification
import eventDemo.contexts.game.application.ports.GameEventBus import eventDemo.contexts.game.application.ports.GameEventBus
import eventDemo.contexts.game.domain.game.GameId
import eventDemo.libs.bus.Bus import eventDemo.libs.bus.Bus
import eventDemo.libs.command.CommandUnicityChecker import eventDemo.libs.command.CommandUnicityChecker
import eventDemo.sharedKernel.UserId import eventDemo.shared.game.command.GameCommand
import eventDemo.shared.game.notification.Notification
import eventDemo.shared.ids.GameId
import eventDemo.shared.ids.UserId
import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@@ -1,7 +1,7 @@
package eventDemo.contexts.game.application.ports package eventDemo.contexts.game.application.ports
import eventDemo.contexts.game.domain.events.GameEvent import eventDemo.contexts.game.domain.events.GameEvent
import eventDemo.contexts.game.domain.game.GameId
import eventDemo.libs.eventSource.eventStore.EventStore import eventDemo.libs.eventSource.eventStore.EventStore
import eventDemo.shared.ids.GameId
interface GameEventStore : EventStore<GameEvent, GameId> interface GameEventStore : EventStore<GameEvent, GameId>
@@ -1,6 +1,6 @@
package eventDemo.domain.event.projection package eventDemo.domain.event.projection
import eventDemo.contexts.game.infrastructure.persistence.projections.models.GameList import eventDemo.shared.game.projection.GameList
interface GameListRepository { interface GameListRepository {
fun getList( fun getList(
@@ -1,6 +1,6 @@
package eventDemo.contexts.game.application.ports package eventDemo.contexts.game.application.ports
import eventDemo.contexts.game.infrastructure.persistence.projections.models.GameProjection
import eventDemo.libs.bus.Bus import eventDemo.libs.bus.Bus
import eventDemo.shared.game.projection.GameProjection
interface GameProjectionBus : Bus<GameProjection> interface GameProjectionBus : Bus<GameProjection>
@@ -9,7 +9,7 @@ import eventDemo.contexts.game.domain.events.NewPlayerEvent
import eventDemo.contexts.game.domain.events.PlayerHaveDrawCardEvent import eventDemo.contexts.game.domain.events.PlayerHaveDrawCardEvent
import eventDemo.contexts.game.domain.events.PlayerReadyEvent import eventDemo.contexts.game.domain.events.PlayerReadyEvent
import eventDemo.contexts.game.domain.events.PlayerWinEvent import eventDemo.contexts.game.domain.events.PlayerWinEvent
import eventDemo.contexts.game.infrastructure.persistence.projections.models.GameList import eventDemo.shared.game.projection.GameList
fun GameList.applyEvent(event: GameEvent): GameList = fun GameList.applyEvent(event: GameEvent): GameList =
when (event) { when (event) {
@@ -1,15 +1,15 @@
package eventDemo.contexts.game.domain.events package eventDemo.contexts.game.domain.events
import eventDemo.contexts.game.domain.game.Card import eventDemo.shared.game.Card
import eventDemo.contexts.game.domain.game.Card.Color import eventDemo.shared.game.Card.Color
import eventDemo.contexts.game.domain.game.GameId import eventDemo.shared.game.Player
import eventDemo.contexts.game.domain.game.Player import eventDemo.shared.ids.EventId
import eventDemo.contexts.game.infrastructure.persistence.serializers.EventIdSerializer import eventDemo.shared.ids.GameId
import eventDemo.libs.eventSource.EventId import eventDemo.shared.serializers.EventIdSerializer
import kotlinx.datetime.Clock import kotlinx.datetime.Clock
import kotlinx.datetime.Instant import kotlinx.datetime.Instant
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import java.util.UUID import kotlin.uuid.Uuid
/** /**
* An [GameEvent] to represent a played card. * An [GameEvent] to represent a played card.
@@ -24,7 +24,7 @@ data class CardIsPlayedEvent(
) : GameEvent, ) : GameEvent,
PlayerActionEvent { PlayerActionEvent {
@Serializable(with = EventIdSerializer::class) @Serializable(with = EventIdSerializer::class)
override val eventId: EventId = EventId(UUID.randomUUID()) override val eventId: EventId = EventId(Uuid.random())
override val createdAt: Instant = Clock.System.now() override val createdAt: Instant = Clock.System.now()
val theColorCard get() = if (card is Card.CardWithColor) card.color else chosenColor val theColorCard get() = if (card is Card.CardWithColor) card.color else chosenColor
@@ -1,14 +1,14 @@
package eventDemo.contexts.game.domain.events package eventDemo.contexts.game.domain.events
import eventDemo.contexts.game.domain.game.DiscardPile import eventDemo.shared.game.DiscardPile
import eventDemo.contexts.game.domain.game.DrawPile import eventDemo.shared.game.DrawPile
import eventDemo.contexts.game.domain.game.GameId import eventDemo.shared.ids.EventId
import eventDemo.contexts.game.infrastructure.persistence.serializers.EventIdSerializer import eventDemo.shared.ids.GameId
import eventDemo.libs.eventSource.EventId import eventDemo.shared.serializers.EventIdSerializer
import kotlinx.datetime.Clock import kotlinx.datetime.Clock
import kotlinx.datetime.Instant import kotlinx.datetime.Instant
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import java.util.UUID import kotlin.uuid.Uuid
/** /**
* When the Pile are shuffled after the draw pille was empty * When the Pile are shuffled after the draw pille was empty
@@ -21,6 +21,6 @@ class DrawFilledWithDiscardEvent(
override val version: Int, override val version: Int,
) : GameEvent { ) : GameEvent {
@Serializable(with = EventIdSerializer::class) @Serializable(with = EventIdSerializer::class)
override val eventId: EventId = EventId(UUID.randomUUID()) override val eventId: EventId = EventId(Uuid.random())
override val createdAt: Instant = Clock.System.now() override val createdAt: Instant = Clock.System.now()
} }
@@ -1,12 +1,12 @@
package eventDemo.contexts.game.domain.events package eventDemo.contexts.game.domain.events
import eventDemo.contexts.game.domain.game.GameId import eventDemo.shared.ids.EventId
import eventDemo.contexts.game.infrastructure.persistence.serializers.EventIdSerializer import eventDemo.shared.ids.GameId
import eventDemo.libs.eventSource.EventId import eventDemo.shared.serializers.EventIdSerializer
import kotlinx.datetime.Clock import kotlinx.datetime.Clock
import kotlinx.datetime.Instant import kotlinx.datetime.Instant
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import java.util.UUID import kotlin.uuid.Uuid
/** /**
* This [GameEvent] is sent when all players are ready. * This [GameEvent] is sent when all players are ready.
@@ -17,6 +17,6 @@ data class GameCreatedEvent(
override val version: Int, override val version: Int,
) : GameEvent { ) : GameEvent {
@Serializable(with = EventIdSerializer::class) @Serializable(with = EventIdSerializer::class)
override val eventId: EventId = EventId(UUID.randomUUID()) override val eventId: EventId = EventId(Uuid.random())
override val createdAt: Instant = Clock.System.now() override val createdAt: Instant = Clock.System.now()
} }
@@ -1,10 +1,10 @@
package eventDemo.contexts.game.domain.events package eventDemo.contexts.game.domain.events
import eventDemo.contexts.game.domain.game.GameId
import eventDemo.contexts.game.infrastructure.persistence.serializers.EventIdSerializer
import eventDemo.contexts.game.infrastructure.persistence.serializers.GameIdSerializer
import eventDemo.libs.eventSource.Event import eventDemo.libs.eventSource.Event
import eventDemo.libs.eventSource.EventId import eventDemo.shared.ids.EventId
import eventDemo.shared.ids.GameId
import eventDemo.shared.serializers.EventIdSerializer
import eventDemo.shared.serializers.GameIdSerializer
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
/** /**
@@ -1,17 +1,17 @@
package eventDemo.contexts.game.domain.events package eventDemo.contexts.game.domain.events
import eventDemo.contexts.game.domain.game.DiscardPile import eventDemo.shared.game.DiscardPile
import eventDemo.contexts.game.domain.game.DrawPile import eventDemo.shared.game.DrawPile
import eventDemo.contexts.game.domain.game.GameId import eventDemo.shared.game.Player
import eventDemo.contexts.game.domain.game.Player import eventDemo.shared.game.PlayerHand
import eventDemo.contexts.game.domain.game.PlayerHand import eventDemo.shared.ids.EventId
import eventDemo.contexts.game.infrastructure.persistence.serializers.EventIdSerializer import eventDemo.shared.ids.GameId
import eventDemo.contexts.game.infrastructure.persistence.serializers.PlayerIdSerializer import eventDemo.shared.serializers.EventIdSerializer
import eventDemo.libs.eventSource.EventId import eventDemo.shared.serializers.PlayerIdSerializer
import kotlinx.datetime.Clock import kotlinx.datetime.Clock
import kotlinx.datetime.Instant import kotlinx.datetime.Instant
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import java.util.UUID import kotlin.uuid.Uuid
/** /**
* This [GameEvent] is sent when all players are ready. * This [GameEvent] is sent when all players are ready.
@@ -27,6 +27,6 @@ data class GameStartedEvent(
override val version: Int, override val version: Int,
) : GameEvent { ) : GameEvent {
@Serializable(with = EventIdSerializer::class) @Serializable(with = EventIdSerializer::class)
override val eventId: EventId = EventId(UUID.randomUUID()) override val eventId: EventId = EventId(Uuid.random())
override val createdAt: Instant = Clock.System.now() override val createdAt: Instant = Clock.System.now()
} }
@@ -1,13 +1,13 @@
package eventDemo.contexts.game.domain.events package eventDemo.contexts.game.domain.events
import eventDemo.contexts.game.domain.game.GameId import eventDemo.shared.game.Player
import eventDemo.contexts.game.domain.game.Player import eventDemo.shared.ids.EventId
import eventDemo.contexts.game.infrastructure.persistence.serializers.EventIdSerializer import eventDemo.shared.ids.GameId
import eventDemo.libs.eventSource.EventId import eventDemo.shared.serializers.EventIdSerializer
import kotlinx.datetime.Clock import kotlinx.datetime.Clock
import kotlinx.datetime.Instant import kotlinx.datetime.Instant
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import java.util.UUID import kotlin.uuid.Uuid
/** /**
* An [GameEvent] to represent a new player joining the game. * An [GameEvent] to represent a new player joining the game.
@@ -22,6 +22,6 @@ data class NewPlayerEvent(
override val playerId: Player.PlayerId get() = player.id override val playerId: Player.PlayerId get() = player.id
@Serializable(with = EventIdSerializer::class) @Serializable(with = EventIdSerializer::class)
override val eventId: EventId = EventId(UUID.randomUUID()) override val eventId: EventId = EventId(Uuid.random())
override val createdAt: Instant = Clock.System.now() override val createdAt: Instant = Clock.System.now()
} }
@@ -1,6 +1,6 @@
package eventDemo.contexts.game.domain.events package eventDemo.contexts.game.domain.events
import eventDemo.contexts.game.domain.game.Player import eventDemo.shared.game.Player
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
@@ -1,15 +1,15 @@
package eventDemo.contexts.game.domain.events package eventDemo.contexts.game.domain.events
import eventDemo.contexts.game.domain.game.Card import eventDemo.shared.game.Card
import eventDemo.contexts.game.domain.game.GameId import eventDemo.shared.game.Player
import eventDemo.contexts.game.domain.game.Player import eventDemo.shared.ids.EventId
import eventDemo.contexts.game.infrastructure.persistence.serializers.EventIdSerializer import eventDemo.shared.ids.GameId
import eventDemo.contexts.game.infrastructure.persistence.serializers.PlayerIdSerializer import eventDemo.shared.serializers.EventIdSerializer
import eventDemo.libs.eventSource.EventId import eventDemo.shared.serializers.PlayerIdSerializer
import kotlinx.datetime.Clock import kotlinx.datetime.Clock
import kotlinx.datetime.Instant import kotlinx.datetime.Instant
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import java.util.UUID import kotlin.uuid.Uuid
/** /**
* This [GameEvent] is sent when a player can play. * This [GameEvent] is sent when a player can play.
@@ -24,6 +24,6 @@ data class PlayerHaveDrawCardEvent(
) : GameEvent, ) : GameEvent,
PlayerActionEvent { PlayerActionEvent {
@Serializable(with = EventIdSerializer::class) @Serializable(with = EventIdSerializer::class)
override val eventId: EventId = EventId(UUID.randomUUID()) override val eventId: EventId = EventId(Uuid.random())
override val createdAt: Instant = Clock.System.now() override val createdAt: Instant = Clock.System.now()
} }
@@ -1,14 +1,14 @@
package eventDemo.contexts.game.domain.events package eventDemo.contexts.game.domain.events
import eventDemo.contexts.game.domain.game.GameId import eventDemo.shared.game.Player
import eventDemo.contexts.game.domain.game.Player import eventDemo.shared.ids.EventId
import eventDemo.contexts.game.infrastructure.persistence.serializers.EventIdSerializer import eventDemo.shared.ids.GameId
import eventDemo.contexts.game.infrastructure.persistence.serializers.PlayerIdSerializer import eventDemo.shared.serializers.EventIdSerializer
import eventDemo.libs.eventSource.EventId import eventDemo.shared.serializers.PlayerIdSerializer
import kotlinx.datetime.Clock import kotlinx.datetime.Clock
import kotlinx.datetime.Instant import kotlinx.datetime.Instant
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import java.util.UUID import kotlin.uuid.Uuid
/** /**
* This [GameEvent] is sent when a player is ready. * This [GameEvent] is sent when a player is ready.
@@ -22,6 +22,6 @@ data class PlayerReadyEvent(
) : GameEvent, ) : GameEvent,
PlayerActionEvent { PlayerActionEvent {
@Serializable(with = EventIdSerializer::class) @Serializable(with = EventIdSerializer::class)
override val eventId: EventId = EventId(UUID.randomUUID()) override val eventId: EventId = EventId(Uuid.random())
override val createdAt: Instant = Clock.System.now() override val createdAt: Instant = Clock.System.now()
} }
@@ -1,14 +1,14 @@
package eventDemo.contexts.game.domain.events package eventDemo.contexts.game.domain.events
import eventDemo.contexts.game.domain.game.GameId import eventDemo.shared.game.Player
import eventDemo.contexts.game.domain.game.Player import eventDemo.shared.ids.EventId
import eventDemo.contexts.game.infrastructure.persistence.serializers.EventIdSerializer import eventDemo.shared.ids.GameId
import eventDemo.contexts.game.infrastructure.persistence.serializers.PlayerIdSerializer import eventDemo.shared.serializers.EventIdSerializer
import eventDemo.libs.eventSource.EventId import eventDemo.shared.serializers.PlayerIdSerializer
import kotlinx.datetime.Clock import kotlinx.datetime.Clock
import kotlinx.datetime.Instant import kotlinx.datetime.Instant
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import java.util.UUID import kotlin.uuid.Uuid
/** /**
* This [GameEvent] is sent when a player is ready. * This [GameEvent] is sent when a player is ready.
@@ -22,6 +22,6 @@ data class PlayerWinEvent(
) : GameEvent, ) : GameEvent,
PlayerActionEvent { PlayerActionEvent {
@Serializable(with = EventIdSerializer::class) @Serializable(with = EventIdSerializer::class)
override val eventId: EventId = EventId(UUID.randomUUID()) override val eventId: EventId = EventId(Uuid.random())
override val createdAt: Instant = Clock.System.now() override val createdAt: Instant = Clock.System.now()
} }
@@ -1,11 +1,11 @@
package eventDemo.contexts.game.domain.game.errors package eventDemo.contexts.game.domain.game.errors
import eventDemo.contexts.game.domain.events.GameEvent import eventDemo.contexts.game.domain.events.GameEvent
import eventDemo.contexts.game.domain.game.Card
import eventDemo.contexts.game.domain.game.Player
import eventDemo.contexts.game.domain.game.PlayerList
import eventDemo.contexts.game.domain.game.gameState.Deck import eventDemo.contexts.game.domain.game.gameState.Deck
import eventDemo.contexts.game.domain.game.gameState.Game import eventDemo.contexts.game.domain.game.gameState.Game
import eventDemo.shared.game.Card
import eventDemo.shared.game.Player
import eventDemo.shared.game.PlayerList
abstract class GameException( abstract class GameException(
message: String, message: String,
@@ -9,10 +9,10 @@ import eventDemo.contexts.game.domain.events.NewPlayerEvent
import eventDemo.contexts.game.domain.events.PlayerHaveDrawCardEvent import eventDemo.contexts.game.domain.events.PlayerHaveDrawCardEvent
import eventDemo.contexts.game.domain.events.PlayerReadyEvent import eventDemo.contexts.game.domain.events.PlayerReadyEvent
import eventDemo.contexts.game.domain.events.PlayerWinEvent import eventDemo.contexts.game.domain.events.PlayerWinEvent
import eventDemo.contexts.game.domain.game.GameId
import eventDemo.contexts.game.domain.game.PlayerList
import eventDemo.contexts.game.domain.game.errors.GameException import eventDemo.contexts.game.domain.game.errors.GameException
import eventDemo.contexts.game.domain.game.errors.InconsistentEventVersionException import eventDemo.contexts.game.domain.game.errors.InconsistentEventVersionException
import eventDemo.shared.game.PlayerList
import eventDemo.shared.ids.GameId
sealed interface Game { sealed interface Game {
val aggregateId: GameId val aggregateId: GameId
@@ -4,18 +4,18 @@ import eventDemo.contexts.game.domain.events.GameEvent
import eventDemo.contexts.game.domain.events.GameStartedEvent import eventDemo.contexts.game.domain.events.GameStartedEvent
import eventDemo.contexts.game.domain.events.NewPlayerEvent import eventDemo.contexts.game.domain.events.NewPlayerEvent
import eventDemo.contexts.game.domain.events.PlayerReadyEvent import eventDemo.contexts.game.domain.events.PlayerReadyEvent
import eventDemo.contexts.game.domain.game.Card
import eventDemo.contexts.game.domain.game.DiscardPile
import eventDemo.contexts.game.domain.game.DrawPile
import eventDemo.contexts.game.domain.game.GameId
import eventDemo.contexts.game.domain.game.Player
import eventDemo.contexts.game.domain.game.PlayerHand
import eventDemo.contexts.game.domain.game.PlayerList
import eventDemo.contexts.game.domain.game.errors.AllPlayerNotReadyException import eventDemo.contexts.game.domain.game.errors.AllPlayerNotReadyException
import eventDemo.contexts.game.domain.game.errors.DeckMissingCardsException import eventDemo.contexts.game.domain.game.errors.DeckMissingCardsException
import eventDemo.contexts.game.domain.game.errors.NeedMorePlayersToStartGameException import eventDemo.contexts.game.domain.game.errors.NeedMorePlayersToStartGameException
import eventDemo.contexts.game.domain.game.errors.ThePlayerIsNotInTheGameException import eventDemo.contexts.game.domain.game.errors.ThePlayerIsNotInTheGameException
import eventDemo.sharedKernel.UserId import eventDemo.shared.game.Card
import eventDemo.shared.game.DiscardPile
import eventDemo.shared.game.DrawPile
import eventDemo.shared.game.Player
import eventDemo.shared.game.PlayerHand
import eventDemo.shared.game.PlayerList
import eventDemo.shared.ids.GameId
import eventDemo.shared.ids.UserId
data class GameCreated( data class GameCreated(
override val aggregateId: GameId, override val aggregateId: GameId,
@@ -1,9 +1,9 @@
package eventDemo.contexts.game.domain.game.gameState package eventDemo.contexts.game.domain.game.gameState
import eventDemo.contexts.game.domain.events.GameEvent import eventDemo.contexts.game.domain.events.GameEvent
import eventDemo.contexts.game.domain.game.GameId import eventDemo.shared.game.Player
import eventDemo.contexts.game.domain.game.Player import eventDemo.shared.game.PlayerList
import eventDemo.contexts.game.domain.game.PlayerList import eventDemo.shared.ids.GameId
data class GameEnded( data class GameEnded(
override val aggregateId: GameId, override val aggregateId: GameId,
@@ -2,8 +2,8 @@ package eventDemo.contexts.game.domain.game.gameState
import eventDemo.contexts.game.domain.events.GameCreatedEvent import eventDemo.contexts.game.domain.events.GameCreatedEvent
import eventDemo.contexts.game.domain.events.GameEvent import eventDemo.contexts.game.domain.events.GameEvent
import eventDemo.contexts.game.domain.game.GameId import eventDemo.shared.game.PlayerList
import eventDemo.contexts.game.domain.game.PlayerList import eventDemo.shared.ids.GameId
data class GameInit( data class GameInit(
override val aggregateId: GameId, override val aggregateId: GameId,
@@ -6,13 +6,6 @@ import eventDemo.contexts.game.domain.events.GameEvent
import eventDemo.contexts.game.domain.events.PlayerActionEvent import eventDemo.contexts.game.domain.events.PlayerActionEvent
import eventDemo.contexts.game.domain.events.PlayerHaveDrawCardEvent import eventDemo.contexts.game.domain.events.PlayerHaveDrawCardEvent
import eventDemo.contexts.game.domain.events.PlayerWinEvent import eventDemo.contexts.game.domain.events.PlayerWinEvent
import eventDemo.contexts.game.domain.game.Card
import eventDemo.contexts.game.domain.game.Card.Color
import eventDemo.contexts.game.domain.game.DiscardPile
import eventDemo.contexts.game.domain.game.DrawPile
import eventDemo.contexts.game.domain.game.GameId
import eventDemo.contexts.game.domain.game.Player
import eventDemo.contexts.game.domain.game.PlayerList
import eventDemo.contexts.game.domain.game.errors.InconsistentGameException import eventDemo.contexts.game.domain.game.errors.InconsistentGameException
import eventDemo.contexts.game.domain.game.errors.ItsNotTheTurnException import eventDemo.contexts.game.domain.game.errors.ItsNotTheTurnException
import eventDemo.contexts.game.domain.game.errors.TheCardHasNoColorException import eventDemo.contexts.game.domain.game.errors.TheCardHasNoColorException
@@ -22,6 +15,13 @@ import eventDemo.contexts.game.domain.game.errors.ThePlayerHasRemainingCardsExce
import eventDemo.contexts.game.domain.game.errors.ThePlayerIsNotInTheGameException import eventDemo.contexts.game.domain.game.errors.ThePlayerIsNotInTheGameException
import eventDemo.contexts.game.domain.game.errors.ThePlayerMustPlayACardException import eventDemo.contexts.game.domain.game.errors.ThePlayerMustPlayACardException
import eventDemo.contexts.game.domain.game.gameState.Game.Direction import eventDemo.contexts.game.domain.game.gameState.Game.Direction
import eventDemo.shared.game.Card
import eventDemo.shared.game.Card.Color
import eventDemo.shared.game.DiscardPile
import eventDemo.shared.game.DrawPile
import eventDemo.shared.game.Player
import eventDemo.shared.game.PlayerList
import eventDemo.shared.ids.GameId
fun PlayerList.nextPlayerTurn( fun PlayerList.nextPlayerTurn(
lastPlayerId: Player.PlayerId, lastPlayerId: Player.PlayerId,
@@ -1,5 +1,6 @@
package eventDemo.contexts.game.infrastructure.configuration.ktor package eventDemo.contexts.game.infrastructure.configuration.ktor
import eventDemo.shared.http.HttpErrorBadRequest
import io.ktor.http.HttpHeaders import io.ktor.http.HttpHeaders
import io.ktor.http.HttpMethod import io.ktor.http.HttpMethod
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
@@ -10,7 +11,6 @@ import io.ktor.server.plugins.cors.routing.CORS
import io.ktor.server.plugins.statuspages.StatusPages import io.ktor.server.plugins.statuspages.StatusPages
import io.ktor.server.resources.Resources import io.ktor.server.resources.Resources
import io.ktor.server.response.respondText import io.ktor.server.response.respondText
import kotlinx.serialization.Serializable
fun Application.configureHttpRouting() { fun Application.configureHttpRouting() {
install(CORS) { install(CORS) {
@@ -38,17 +38,3 @@ fun Application.configureHttpRouting() {
class BadRequestException( class BadRequestException(
val httpError: HttpErrorBadRequest, val httpError: HttpErrorBadRequest,
) : Exception() ) : Exception()
@Serializable
class HttpErrorBadRequest(
val title: String = HttpStatusCode.BadRequest.description,
val invalidParams: List<InvalidParam> = emptyList(),
) {
val statusCode: Int = HttpStatusCode.BadRequest.value
@Serializable
data class InvalidParam(
val name: String,
val reason: String,
)
}
@@ -1,21 +1,21 @@
package eventDemo.contexts.game.infrastructure.configuration.ktor package eventDemo.contexts.game.infrastructure.configuration.ktor
import eventDemo.contexts.game.domain.game.GameId import eventDemo.shared.game.Player
import eventDemo.contexts.game.domain.game.Player import eventDemo.shared.ids.CommandId
import eventDemo.contexts.game.infrastructure.persistence.serializers.CommandIdSerializer import eventDemo.shared.ids.EventId
import eventDemo.contexts.game.infrastructure.persistence.serializers.EventIdSerializer import eventDemo.shared.ids.GameId
import eventDemo.contexts.game.infrastructure.persistence.serializers.GameIdSerializer import eventDemo.shared.serializers.CommandIdSerializer
import eventDemo.contexts.game.infrastructure.persistence.serializers.PlayerIdSerializer import eventDemo.shared.serializers.EventIdSerializer
import eventDemo.libs.command.CommandId import eventDemo.shared.serializers.GameIdSerializer
import eventDemo.libs.eventSource.EventId import eventDemo.shared.serializers.PlayerIdSerializer
import eventDemo.libs.serializer.UUIDSerializer import eventDemo.shared.serializers.UUIDSerializer
import io.ktor.serialization.kotlinx.json.json import io.ktor.serialization.kotlinx.json.json
import io.ktor.server.application.Application import io.ktor.server.application.Application
import io.ktor.server.application.install import io.ktor.server.application.install
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.modules.SerializersModule import kotlinx.serialization.modules.SerializersModule
import java.util.UUID import kotlin.uuid.Uuid
fun Application.configureSerialization() { fun Application.configureSerialization() {
install(ContentNegotiation) { install(ContentNegotiation) {
@@ -29,7 +29,7 @@ fun defaultJsonSerializer(): Json =
Json { Json {
serializersModule = serializersModule =
SerializersModule { SerializersModule {
contextual(UUID::class) { UUIDSerializer } contextual(Uuid::class) { UUIDSerializer }
contextual(GameId::class) { GameIdSerializer } contextual(GameId::class) { GameIdSerializer }
contextual(EventId::class) { EventIdSerializer } contextual(EventId::class) { EventIdSerializer }
contextual(CommandId::class) { CommandIdSerializer } contextual(CommandId::class) { CommandIdSerializer }
@@ -2,9 +2,9 @@ package eventDemo.contexts.game.infrastructure.persistence.eventStore
import eventDemo.contexts.game.application.ports.GameEventStore import eventDemo.contexts.game.application.ports.GameEventStore
import eventDemo.contexts.game.domain.events.GameEvent import eventDemo.contexts.game.domain.events.GameEvent
import eventDemo.contexts.game.domain.game.GameId
import eventDemo.libs.eventSource.eventStore.EventStore import eventDemo.libs.eventSource.eventStore.EventStore
import eventDemo.libs.eventSource.eventStore.EventStoreInMemory import eventDemo.libs.eventSource.eventStore.EventStoreInMemory
import eventDemo.shared.ids.GameId
/** /**
* A stream to publish and read the played card event. * A stream to publish and read the played card event.
@@ -2,9 +2,9 @@ package eventDemo.contexts.game.infrastructure.persistence.eventStore
import eventDemo.contexts.game.application.ports.GameEventStore import eventDemo.contexts.game.application.ports.GameEventStore
import eventDemo.contexts.game.domain.events.GameEvent import eventDemo.contexts.game.domain.events.GameEvent
import eventDemo.contexts.game.domain.game.GameId
import eventDemo.libs.eventSource.eventStore.EventStore import eventDemo.libs.eventSource.eventStore.EventStore
import eventDemo.libs.eventSource.eventStore.EventStoreInPostgresql import eventDemo.libs.eventSource.eventStore.EventStoreInPostgresql
import eventDemo.shared.ids.GameId
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import javax.sql.DataSource import javax.sql.DataSource
@@ -4,9 +4,9 @@ import eventDemo.contexts.game.application.ports.GameEventBus
import eventDemo.contexts.game.application.ports.GameEventStore import eventDemo.contexts.game.application.ports.GameEventStore
import eventDemo.contexts.game.application.ports.GameProjectionBus import eventDemo.contexts.game.application.ports.GameProjectionBus
import eventDemo.contexts.game.application.projections.applyEvent import eventDemo.contexts.game.application.projections.applyEvent
import eventDemo.contexts.game.domain.game.GameId
import eventDemo.contexts.game.infrastructure.persistence.projections.models.GameList
import eventDemo.domain.event.projection.GameListRepository import eventDemo.domain.event.projection.GameListRepository
import eventDemo.shared.game.projection.GameList
import eventDemo.shared.ids.GameId
import io.github.oshai.kotlinlogging.withLoggingContext import io.github.oshai.kotlinlogging.withLoggingContext
/** /**
@@ -1,9 +1,9 @@
package eventDemo.contexts.game.infrastructure.persistence.projections.bus package eventDemo.contexts.game.infrastructure.persistence.projections.bus
import eventDemo.contexts.game.application.ports.GameProjectionBus import eventDemo.contexts.game.application.ports.GameProjectionBus
import eventDemo.contexts.game.infrastructure.persistence.projections.models.GameProjection
import eventDemo.libs.bus.Bus import eventDemo.libs.bus.Bus
import eventDemo.libs.bus.BusInMemory import eventDemo.libs.bus.BusInMemory
import eventDemo.shared.game.projection.GameProjection
import java.util.UUID import java.util.UUID
class GameProjectionBusInMemory : class GameProjectionBusInMemory :
@@ -2,9 +2,9 @@ package eventDemo.contexts.game.infrastructure.persistence.projections.bus
import com.rabbitmq.client.ConnectionFactory import com.rabbitmq.client.ConnectionFactory
import eventDemo.contexts.game.application.ports.GameProjectionBus import eventDemo.contexts.game.application.ports.GameProjectionBus
import eventDemo.contexts.game.infrastructure.persistence.projections.models.GameProjection
import eventDemo.libs.bus.Bus import eventDemo.libs.bus.Bus
import eventDemo.libs.bus.BusInRabbitMQ import eventDemo.libs.bus.BusInRabbitMQ
import eventDemo.shared.game.projection.GameProjection
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import java.util.UUID import java.util.UUID
@@ -3,8 +3,8 @@ package eventDemo.contexts.game.infrastructure.rest
import eventDemo.contexts.game.application.eventStores.GameRepository import eventDemo.contexts.game.application.eventStores.GameRepository
import eventDemo.contexts.game.application.notification.toNotification import eventDemo.contexts.game.application.notification.toNotification
import eventDemo.contexts.game.application.ports.GameEventStore import eventDemo.contexts.game.application.ports.GameEventStore
import eventDemo.contexts.game.domain.game.GameId import eventDemo.shared.ids.GameId
import eventDemo.contexts.game.infrastructure.persistence.serializers.GameIdSerializer import eventDemo.shared.serializers.GameIdSerializer
import eventDemo.sharedKernel.currentUserId import eventDemo.sharedKernel.currentUserId
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
import io.ktor.resources.Resource import io.ktor.resources.Resource
@@ -1,22 +1,22 @@
package eventDemo.contexts.game.infrastructure.websocket package eventDemo.contexts.game.infrastructure.websocket
import eventDemo.contexts.game.application.channels.GameChannelsSubscriber import eventDemo.contexts.game.application.channels.GameChannelsSubscriber
import eventDemo.contexts.game.domain.game.GameId
import eventDemo.libs.helpers.fromFrameChannel import eventDemo.libs.helpers.fromFrameChannel
import eventDemo.libs.helpers.toObjectChannel import eventDemo.libs.helpers.toObjectChannel
import eventDemo.shared.ids.GameId
import eventDemo.sharedKernel.currentUserId import eventDemo.sharedKernel.currentUserId
import io.ktor.server.auth.authenticate import io.ktor.server.auth.authenticate
import io.ktor.server.routing.Route import io.ktor.server.routing.Route
import io.ktor.server.websocket.webSocket import io.ktor.server.websocket.webSocket
import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.DelicateCoroutinesApi
import java.util.UUID import kotlin.uuid.Uuid
@DelicateCoroutinesApi @DelicateCoroutinesApi
fun Route.gameWebSocket(channelSubscriber: GameChannelsSubscriber) { fun Route.gameWebSocket(channelSubscriber: GameChannelsSubscriber) {
authenticate { authenticate {
webSocket("/games/{id}") { webSocket("/games/{id}") {
channelSubscriber.subscribePlayerToGameChannels( channelSubscriber.subscribePlayerToGameChannels(
gameId = GameId(UUID.fromString(call.parameters["id"]!!)), gameId = GameId(Uuid.parse(call.parameters["id"]!!)),
userId = call.currentUserId, userId = call.currentUserId,
incomingCommandChannel = toObjectChannel(incoming), incomingCommandChannel = toObjectChannel(incoming),
sendNotificationChannel = fromFrameChannel(outgoing), sendNotificationChannel = fromFrameChannel(outgoing),
@@ -1,5 +1,7 @@
package eventDemo.libs.command package eventDemo.libs.command
import eventDemo.shared.command.Command
import eventDemo.shared.ids.CommandId
import kotlinx.datetime.Clock import kotlinx.datetime.Clock
import kotlinx.datetime.Instant import kotlinx.datetime.Instant
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
@@ -0,0 +1,16 @@
package eventDemo.libs.eventSource
import eventDemo.shared.ids.AggregateId
import eventDemo.shared.ids.EventId
import kotlinx.datetime.Instant
/**
* The basic interface for an Event
* @see eventDemo.libs.eventSource.eventStore.EventStream
*/
interface Event<ID : AggregateId> {
val eventId: EventId
val aggregateId: ID
val createdAt: Instant
val version: Int
}
@@ -1,7 +1,7 @@
package eventDemo.libs.eventSource.eventStore package eventDemo.libs.eventSource.eventStore
import eventDemo.libs.eventSource.AggregateId
import eventDemo.libs.eventSource.Event import eventDemo.libs.eventSource.Event
import eventDemo.shared.ids.AggregateId
import io.github.oshai.kotlinlogging.withLoggingContext import io.github.oshai.kotlinlogging.withLoggingContext
interface EventStore<E : Event<ID>, ID : AggregateId> { interface EventStore<E : Event<ID>, ID : AggregateId> {
@@ -1,7 +1,7 @@
package eventDemo.libs.eventSource.eventStore package eventDemo.libs.eventSource.eventStore
import eventDemo.libs.eventSource.AggregateId
import eventDemo.libs.eventSource.Event import eventDemo.libs.eventSource.Event
import eventDemo.shared.ids.AggregateId
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentMap import java.util.concurrent.ConcurrentMap
@@ -1,7 +1,7 @@
package eventDemo.libs.eventSource.eventStore package eventDemo.libs.eventSource.eventStore
import eventDemo.libs.eventSource.AggregateId
import eventDemo.libs.eventSource.Event import eventDemo.libs.eventSource.Event
import eventDemo.shared.ids.AggregateId
import javax.sql.DataSource import javax.sql.DataSource
class EventStoreInPostgresql<E : Event<ID>, ID : AggregateId>( class EventStoreInPostgresql<E : Event<ID>, ID : AggregateId>(
@@ -1,7 +1,7 @@
package eventDemo.libs.eventSource.eventStore package eventDemo.libs.eventSource.eventStore
import eventDemo.libs.eventSource.AggregateId
import eventDemo.libs.eventSource.Event import eventDemo.libs.eventSource.Event
import eventDemo.shared.ids.AggregateId
import io.github.oshai.kotlinlogging.withLoggingContext import io.github.oshai.kotlinlogging.withLoggingContext
/** /**
@@ -1,7 +1,7 @@
package eventDemo.libs.eventSource.eventStore package eventDemo.libs.eventSource.eventStore
import eventDemo.libs.eventSource.AggregateId
import eventDemo.libs.eventSource.Event import eventDemo.libs.eventSource.Event
import eventDemo.shared.ids.AggregateId
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import java.util.Queue import java.util.Queue
import java.util.concurrent.ConcurrentLinkedQueue import java.util.concurrent.ConcurrentLinkedQueue
@@ -1,12 +1,13 @@
package eventDemo.libs.eventSource.eventStore package eventDemo.libs.eventSource.eventStore
import eventDemo.libs.eventSource.AggregateId
import eventDemo.libs.eventSource.Event import eventDemo.libs.eventSource.Event
import eventDemo.shared.ids.AggregateId
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import io.github.oshai.kotlinlogging.withLoggingContext import io.github.oshai.kotlinlogging.withLoggingContext
import org.postgresql.util.PGobject import org.postgresql.util.PGobject
import org.postgresql.util.PSQLException import org.postgresql.util.PSQLException
import javax.sql.DataSource import javax.sql.DataSource
import kotlin.uuid.toJavaUuid
/** /**
* An In-Memory implementation of an event stream. * An In-Memory implementation of an event stream.
@@ -39,8 +40,8 @@ class EventStreamInPostgresql<E : Event<ID>, ID : AggregateId>(
on conflict (id) do nothing on conflict (id) do nothing
""".trimIndent(), """.trimIndent(),
).use { ).use {
it.setObject(1, event.eventId.id) it.setObject(1, event.eventId.id.toJavaUuid())
it.setObject(2, event.aggregateId.id) it.setObject(2, event.aggregateId.id.toJavaUuid())
it.setInt(3, event.version) it.setInt(3, event.version)
it.setObject(4, PGJsonb(objectToString(event))) it.setObject(4, PGJsonb(objectToString(event)))
it.executeUpdate() it.executeUpdate()
@@ -69,7 +70,7 @@ class EventStreamInPostgresql<E : Event<ID>, ID : AggregateId>(
order by version asc order by version asc
""".trimIndent(), """.trimIndent(),
).use { ).use {
it.setObject(1, aggregateId.id) it.setObject(1, aggregateId.id.toJavaUuid())
it.executeQuery().use { resultSet -> it.executeQuery().use { resultSet ->
buildSet { buildSet {
while (resultSet.next()) { while (resultSet.next()) {
@@ -95,7 +96,7 @@ class EventStreamInPostgresql<E : Event<ID>, ID : AggregateId>(
order by version asc order by version asc
""".trimIndent(), """.trimIndent(),
).use { ).use {
it.setObject(1, aggregateId.id) it.setObject(1, aggregateId.id.toJavaUuid())
it.executeQuery().use { resultSet -> it.executeQuery().use { resultSet ->
resultSet.next() resultSet.next()
} }
@@ -116,7 +117,7 @@ class EventStreamInPostgresql<E : Event<ID>, ID : AggregateId>(
).use { stmt -> ).use { stmt ->
stmt.setInt(1, version.first) stmt.setInt(1, version.first)
stmt.setInt(2, version.last) stmt.setInt(2, version.last)
stmt.setObject(3, aggregateId.id) stmt.setObject(3, aggregateId.id.toJavaUuid())
stmt.executeQuery().use { resultSet -> stmt.executeQuery().use { resultSet ->
buildSet { buildSet {
while (resultSet.next()) { while (resultSet.next()) {

Some files were not shown because too many files have changed in this diff Show More