Skip to content

Commit 16c8798

Browse files
authored
Merge pull request #70 from fraktalio/feature/metadata
`Metadata` flavour of Fmodel API
2 parents e36011d + d6a9fda commit 16c8798

File tree

6 files changed

+63
-34
lines changed

6 files changed

+63
-34
lines changed

build.gradle.kts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,11 @@ configurations {
2424

2525
repositories {
2626
mavenCentral()
27+
maven("https://s01.oss.sonatype.org/content/repositories/snapshots/")
2728
}
2829

2930
extra["testcontainersVersion"] = "1.19.3"
30-
extra["fmodelVersion"] = "3.5.0"
31+
extra["fmodelVersion"] = "3.5.1-SNAPSHOT"
3132
extra["kotlinxSerializationJson"] = "1.6.1"
3233
extra["kotlinxCollectionsImmutable"] = "0.3.6"
3334
extra["kotlinLogging"] = "3.0.5"

src/main/kotlin/com/fraktalio/example/fmodelspringdemo/adapter/persistence/AggregateEventRepositoryImpl.kt

Lines changed: 50 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -90,16 +90,19 @@ class AggregateEventRepositoryImpl(
9090
* Fetching the current state as a series/flow of Events
9191
*/
9292
override fun Command?.fetchEvents(): Flow<Pair<Event, UUID?>> =
93+
fetchEventsAndMetaData().map { Pair(it.first, it.second) }
94+
95+
/**
96+
* Fetching the current state as a series/flow of Events and Metadata.
97+
* This method is implemented by Default in the EventRepository interface, but we need/want to override it here.
98+
*/
99+
override fun Command?.fetchEventsAndMetaData(): Flow<Triple<Event, UUID?, Map<String, Any>>> =
93100
when (this) {
94-
is Command -> getEvents(deciderId()).map { it.toEventWithId() }
101+
is Command -> getEvents(deciderId()).map { it.toEventWithIdAndMetaData() }
102+
.map { Triple(it.first, it.second, mapOf("commandId" to it.third as Any)) }
103+
95104
null -> emptyFlow()
96105
}
97-
.onCompletion {
98-
when (it) {
99-
null -> logger.debug { "fetching the aggregate events by command ${this@fetchEvents} completed with success" }
100-
else -> logger.warn { "fetching the aggregate events by command ${this@fetchEvents} completed with exception $it" }
101-
}
102-
}
103106
.flowOn(dbDispatcher)
104107

105108
/**
@@ -117,54 +120,71 @@ class AggregateEventRepositoryImpl(
117120
*
118121
* `latestVersionProvider` is used to fetch the latest version of the event stream, per need
119122
*/
120-
override fun Flow<Event?>.save(latestVersionProvider: (Event?) -> UUID?): Flow<Pair<Event, UUID>> = flow {
123+
override fun Flow<Event?>.save(latestVersionProvider: (Event?) -> UUID?): Flow<Pair<Event, UUID>> =
124+
saveWithMetaData(latestVersionProvider, emptyMap()).map { Pair(it.first, it.second) }
125+
126+
/**
127+
* Storing the new state as a series/flow of Events and Metadata
128+
* This method is implemented by Default in the EventRepository interface, but we need/want to override it here.
129+
*
130+
* `latestVersionProvider` is used to fetch the latest version of the event stream, per need
131+
*/
132+
override fun Flow<Event?>.saveWithMetaData(
133+
latestVersionProvider: (Event?) -> UUID?,
134+
metaData: Map<String, Any>
135+
): Flow<Triple<Event, UUID, Map<String, Any>>> = flow {
121136
val previousIds: MutableMap<String, UUID?> = emptyMap<String, UUID?>().toMutableMap()
122137
emitAll(
123138
filterNotNull()
124139
.map {
125140
previousIds.computeIfAbsent(it.deciderId()) { _ -> latestVersionProvider(it) }
126141
val eventId = UUID.randomUUID()
127-
val eventEntity = it.toEventEntity(eventId, previousIds[it.deciderId()])
142+
val eventEntity =
143+
it.toEventEntity(eventId, previousIds[it.deciderId()], metaData["commandId"] as UUID)
128144
previousIds[it.deciderId()] = eventId
129145
eventEntity
130146
}
131147
.appendAll()
132-
.map { it.toEventWithId() }
148+
.map { it.toEventWithIdAndMetaData() }
149+
.map { Triple(it.first, it.second, mapOf("commandId" to it.third as Any)) }
133150
)
134151
}
135-
.onCompletion {
136-
when (it) {
137-
null -> logger.debug { "saving new events completed successfully" }
138-
else -> logger.warn { "saving new events completed with exception $it" }
139-
}
140-
}
141152
.flowOn(dbDispatcher)
142153

143154
/**
144155
* Storing the new state as a series/flow of Events
145156
*
146157
* `latestVersion` is used to provide you with the latest known version of the state/stream
147158
*/
148-
override fun Flow<Event?>.save(latestVersion: UUID?): Flow<Pair<Event, UUID>> = flow {
159+
override fun Flow<Event?>.save(latestVersion: UUID?): Flow<Pair<Event, UUID>> =
160+
saveWithMetaData(latestVersion, emptyMap()).map { Pair(it.first, it.second) }
161+
162+
/**
163+
* Storing the new state as a series/flow of Events with metadata
164+
* This method is implemented by Default in the EventRepository interface, but we need/want to override it here.
165+
*
166+
*
167+
* `latestVersion` is used to provide you with the latest known version of the state/stream
168+
* `metaData` is used to provide you with the metadata
169+
*/
170+
override fun Flow<Event?>.saveWithMetaData(
171+
latestVersion: UUID?,
172+
metaData: Map<String, Any>
173+
): Flow<Triple<Event, UUID, Map<String, Any>>> = flow {
149174
var previousId: UUID? = latestVersion
150175
emitAll(
151176
filterNotNull()
152177
.map {
153178
val eventId = UUID.randomUUID()
154-
val eventEntity = it.toEventEntity(eventId, previousId)
179+
val eventEntity = it.toEventEntity(eventId, previousId, metaData["commandId"] as UUID)
155180
previousId = eventId
156181
eventEntity
157182
}
158183
.appendAll()
159-
.map { it.toEventWithId() }
184+
.map { it.toEventWithIdAndMetaData() }
185+
.map { Triple(it.first, it.second, mapOf("commandId" to it.third as Any)) }
160186
)
161187
}
162-
.onCompletion {
163-
when (it) {
164-
null -> logger.debug { "saving new events completed successfully" }
165-
else -> logger.warn { "saving new events completed with exception $it" }
166-
}
167-
}
168188
.flowOn(dbDispatcher)
169189
}
170190

@@ -175,7 +195,7 @@ internal data class EventEntity(
175195
val event: String,
176196
val data: ByteArray,
177197
val eventId: UUID,
178-
val commandId: UUID?,
198+
val commandId: UUID,
179199
val previousId: UUID?,
180200
val final: Boolean,
181201
val createdAt: OffsetDateTime? = null,
@@ -198,7 +218,10 @@ internal val eventMapper: (Row, RowMetadata) -> EventEntity = { row, _ ->
198218
}
199219

200220
internal fun EventEntity.toEventWithId() = Pair<Event, UUID>(Json.decodeFromString(data.decodeToString()), eventId)
201-
internal fun Event.toEventEntity(eventId: UUID, previousId: UUID?, commandId: UUID? = null) = EventEntity(
221+
internal fun EventEntity.toEventWithIdAndMetaData() =
222+
Triple<Event, UUID, UUID?>(Json.decodeFromString(data.decodeToString()), eventId, commandId)
223+
224+
internal fun Event.toEventEntity(eventId: UUID, previousId: UUID?, commandId: UUID) = EventEntity(
202225
decider(),
203226
deciderId(),
204227
event(),

src/main/kotlin/com/fraktalio/example/fmodelspringdemo/adapter/web/rest/AggregateRestCommandController.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ class AggregateRestCommandController(private val aggregate: Aggregate) {
2020

2121
@OptIn(ExperimentalCoroutinesApi::class)
2222
private fun handle(command: Command): Flow<Event?> =
23-
aggregate.handleOptimistically(command).map { it.first }
23+
aggregate.handleOptimistically(command, mapOf("commandId" to UUID.randomUUID())).map { it.first }
24+
2425

2526
@PostMapping("restaurants")
2627
fun createRestaurant(@RequestBody command: CreateRestaurantCommand): Flow<Event?> =

src/main/kotlin/com/fraktalio/example/fmodelspringdemo/adapter/web/rsocket/AggregateRsocketCommandController.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,20 @@ package com.fraktalio.example.fmodelspringdemo.adapter.web.rsocket
33
import com.fraktalio.example.fmodelspringdemo.application.Aggregate
44
import com.fraktalio.example.fmodelspringdemo.domain.Command
55
import com.fraktalio.example.fmodelspringdemo.domain.Event
6-
import com.fraktalio.fmodel.application.handleOptimistically
6+
import com.fraktalio.fmodel.application.handleOptimisticallyWithMetaData
77
import kotlinx.coroutines.ExperimentalCoroutinesApi
88
import kotlinx.coroutines.flow.Flow
99
import kotlinx.coroutines.flow.map
1010
import org.springframework.messaging.handler.annotation.MessageMapping
1111
import org.springframework.messaging.handler.annotation.Payload
1212
import org.springframework.stereotype.Controller
13+
import java.util.*
1314

1415
@Controller
1516
class AggregateRsocketCommandController(private val aggregate: Aggregate) {
1617
@OptIn(ExperimentalCoroutinesApi::class)
1718
@MessageMapping("commands")
1819
fun handleCommand(@Payload commands: Flow<Command>): Flow<Event?> =
19-
aggregate.handleOptimistically(commands).map { it.first }
20+
aggregate.handleOptimisticallyWithMetaData(commands.map { Pair(it, mapOf("commandId" to UUID.randomUUID())) })
21+
.map { it.first }
2022
}

src/main/resources/sql/event_sourcing.sql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ CREATE TABLE IF NOT EXISTS events
2727
-- event data in JSON format
2828
"data" JSONB NOT NULL,
2929
-- command ID causing this event
30-
"command_id" UUID NULL,
30+
"command_id" UUID NOT NULL,
3131
-- previous event uuid; null for first event; null does not trigger UNIQUE constraint; we defined a function `check_first_event_for_decider`
3232
"previous_id" UUID UNIQUE,
3333
-- indicator if the event stream for the `decider_id` is final

src/test/kotlin/com/fraktalio/example/fmodelspringdemo/application/AggregateTest.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package com.fraktalio.example.fmodelspringdemo.application
22

33
import com.fraktalio.example.fmodelspringdemo.domain.*
4-
import com.fraktalio.fmodel.application.publishOptimisticallyTo
4+
import com.fraktalio.fmodel.application.publishOptimisticallyWithMetaDataTo
55
import kotlinx.collections.immutable.toImmutableList
66
import kotlinx.coroutines.ExperimentalCoroutinesApi
77
import kotlinx.coroutines.FlowPreview
@@ -16,6 +16,7 @@ import org.springframework.boot.test.context.SpringBootTest
1616
import org.springframework.test.context.TestConstructor
1717
import org.springframework.test.context.TestConstructor.AutowireMode.ALL
1818
import java.math.BigDecimal
19+
import java.util.*
1920

2021

2122
@SpringBootTest
@@ -47,7 +48,8 @@ class AggregateTest(private val aggregate: Aggregate) {
4748

4849
val events =
4950
flowOf(createRestaurantCommand, changeRestaurantMenuCommand, placeOrderCommand, markOrderAsPreparedCommand)
50-
.publishOptimisticallyTo(aggregate)
51+
.map { Pair(it, mapOf("commandId" to UUID.randomUUID())) }
52+
.publishOptimisticallyWithMetaDataTo(aggregate)
5153
.map { it.first }
5254
.toList()
5355

0 commit comments

Comments
 (0)