@@ -4,13 +4,10 @@ import arrow.core.Either
44import arrow.core.raise.either
55import arrow.core.raise.ensure
66import arrow.core.raise.ensureNotNull
7- import io.github.kamiazya.scopes.platform.domain.aggregate.AggregateResult
8- import io.github.kamiazya.scopes.platform.domain.aggregate.AggregateRoot
9- import io.github.kamiazya.scopes.platform.domain.event.EventEnvelope
10- import io.github.kamiazya.scopes.platform.domain.event.evolveWithPending
117import io.github.kamiazya.scopes.platform.domain.value.AggregateId
128import io.github.kamiazya.scopes.platform.domain.value.AggregateVersion
139import io.github.kamiazya.scopes.platform.domain.value.EventId
10+ import org.jmolecules.ddd.types.AggregateRoot
1411import io.github.kamiazya.scopes.scopemanagement.domain.entity.Scope
1512import io.github.kamiazya.scopes.scopemanagement.domain.error.ScopeError
1613import io.github.kamiazya.scopes.scopemanagement.domain.error.ScopesError
@@ -33,7 +30,7 @@ import kotlinx.datetime.Clock
3330import kotlinx.datetime.Instant
3431
3532/* *
36- * Scope aggregate root implementing event sourcing pattern.
33+ * Scope aggregate root implementing event sourcing pattern with jMolecules DDD types .
3734 *
3835 * This aggregate encapsulates all business logic related to Scopes,
3936 * ensuring that all state changes go through proper domain events.
@@ -44,16 +41,66 @@ import kotlinx.datetime.Instant
4441 * - Business rules are validated before generating events
4542 * - The aggregate can be reconstructed from its event history
4643 * - Commands return new instances (immutability)
44+ * - Implements jMolecules AggregateRoot<ScopeAggregate, ScopeId> for explicit DDD modeling
4745 */
4846data class ScopeAggregate (
49- override val id : AggregateId ,
50- override val version : AggregateVersion ,
47+ private val _id : ScopeId ,
48+ @Deprecated(" Use ScopeId directly instead of AggregateId" , ReplaceWith (" _id" ))
49+ val aggregateId : AggregateId ,
50+ val version : AggregateVersion ,
5151 val createdAt : Instant ,
5252 val updatedAt : Instant ,
5353 val scope : Scope ? ,
5454 val isDeleted : Boolean = false ,
5555 val isArchived : Boolean = false ,
56- ) : AggregateRoot<ScopeAggregate, ScopeEvent>() {
56+ ) : AggregateRoot<ScopeAggregate, ScopeId> {
57+
58+ override fun getId (): ScopeId = _id
59+
60+ // Event sourcing support - uncommitted events tracking
61+ private val uncommittedEventsList = mutableListOf<ScopeEvent >()
62+ val uncommittedEvents: List <ScopeEvent > get() = uncommittedEventsList.toList()
63+
64+ /* *
65+ * Raise a new domain event and apply it to update state.
66+ */
67+ protected fun raiseEvent (event : ScopeEvent ): ScopeAggregate {
68+ uncommittedEventsList.add(event)
69+ return applyEvent(event)
70+ }
71+
72+ /* *
73+ * Mark all uncommitted events as committed.
74+ * Called after successful persistence.
75+ */
76+ fun markEventsAsCommitted () {
77+ uncommittedEventsList.clear()
78+ }
79+
80+ /* *
81+ * Get and clear uncommitted events atomically.
82+ */
83+ fun getAndClearUncommittedEvents (): List <ScopeEvent > {
84+ val events = uncommittedEventsList.toList()
85+ uncommittedEventsList.clear()
86+ return events
87+ }
88+
89+ /* *
90+ * Check if there are any uncommitted changes.
91+ */
92+ fun hasUncommittedChanges (): Boolean = uncommittedEventsList.isNotEmpty()
93+
94+ /* *
95+ * Replay events to rebuild aggregate state.
96+ */
97+ fun replayEvents (events : List <ScopeEvent >): ScopeAggregate {
98+ var aggregate = this
99+ for (event in events) {
100+ aggregate = aggregate.applyEvent(event)
101+ }
102+ return aggregate
103+ }
57104
58105 companion object {
59106 /* *
@@ -85,7 +132,8 @@ data class ScopeAggregate(
85132 )
86133
87134 val initialAggregate = ScopeAggregate (
88- id = aggregateId,
135+ _id = scopeId,
136+ aggregateId = aggregateId,
89137 version = AggregateVersion .initial(),
90138 createdAt = now,
91139 updatedAt = now,
@@ -97,63 +145,14 @@ data class ScopeAggregate(
97145 initialAggregate.raiseEvent(event)
98146 }
99147
100- /* *
101- * Creates a scope using decide/evolve pattern.
102- * Returns an AggregateResult with the new aggregate and pending events.
103- */
104- fun handleCreate (
105- title : String ,
106- description : String? = null,
107- parentId : ScopeId ? = null,
108- scopeId : ScopeId ? = null,
109- now : Instant = Clock .System .now(),
110- ): Either <ScopesError , AggregateResult <ScopeAggregate , ScopeEvent >> = either {
111- val validatedTitle = ScopeTitle .create(title).bind()
112- val validatedDescription = ScopeDescription .create(description).bind()
113- val scopeId = scopeId ? : ScopeId .generate()
114- val aggregateId = scopeId.toAggregateId().bind()
115-
116- val initialAggregate = ScopeAggregate (
117- id = aggregateId,
118- version = AggregateVersion .initial(),
119- createdAt = now,
120- updatedAt = now,
121- scope = null ,
122- isDeleted = false ,
123- isArchived = false ,
124- )
125-
126- // Decide phase - create events with dummy version
127- val event = ScopeCreated (
128- aggregateId = aggregateId,
129- eventId = EventId .generate(),
130- occurredAt = now,
131-
132- aggregateVersion = AggregateVersion .initial(), // Dummy version
133- scopeId = scopeId,
134- title = validatedTitle,
135- description = validatedDescription,
136- parentId = parentId,
137- )
138-
139- val pendingEvents = listOf (EventEnvelope .Pending (event))
140-
141- // Evolve phase - apply events to aggregate
142- val evolvedAggregate = initialAggregate.evolveWithPending(pendingEvents)
143-
144- AggregateResult (
145- aggregate = evolvedAggregate,
146- events = pendingEvents,
147- baseVersion = AggregateVersion .initial(),
148- )
149- }
150148
151149 /* *
152150 * Creates an empty aggregate for event replay.
153151 * Used when loading an aggregate from the event store.
154152 */
155- fun empty (aggregateId : AggregateId ): ScopeAggregate = ScopeAggregate (
156- id = aggregateId,
153+ fun empty (scopeId : ScopeId , aggregateId : AggregateId ): ScopeAggregate = ScopeAggregate (
154+ _id = scopeId,
155+ aggregateId = aggregateId,
157156 version = AggregateVersion .initial(),
158157 createdAt = Instant .DISTANT_PAST ,
159158 updatedAt = Instant .DISTANT_PAST ,
@@ -182,7 +181,7 @@ data class ScopeAggregate(
182181 }
183182
184183 val event = ScopeTitleUpdated (
185- aggregateId = id ,
184+ aggregateId = aggregateId ,
186185 eventId = EventId .generate(),
187186 occurredAt = now,
188187
@@ -195,64 +194,6 @@ data class ScopeAggregate(
195194 this @ScopeAggregate.raiseEvent(event)
196195 }
197196
198- /* *
199- * Decides whether to update the title (decide phase).
200- * Returns pending events or empty list if no change needed.
201- */
202- fun decideUpdateTitle (title : String , now : Instant = Clock .System .now()): Either <ScopesError , List <EventEnvelope .Pending <ScopeEvent >>> = either {
203- val currentScope = scope
204- ensureNotNull(currentScope) {
205- ScopeError .NotFound (ScopeId .create(id.value.substringAfterLast(" /" )).bind())
206- }
207- ensure(! isDeleted) {
208- ScopeError .AlreadyDeleted (currentScope.id)
209- }
210-
211- val newTitle = ScopeTitle .create(title).bind()
212- if (currentScope.title == newTitle) {
213- return @either emptyList()
214- }
215-
216- val event = ScopeTitleUpdated (
217- aggregateId = id,
218- eventId = EventId .generate(),
219- occurredAt = now,
220-
221- aggregateVersion = AggregateVersion .initial(), // Dummy version
222- scopeId = currentScope.id,
223- oldTitle = currentScope.title,
224- newTitle = newTitle,
225- )
226-
227- listOf (EventEnvelope .Pending (event))
228- }
229-
230- /* *
231- * Handles update title command using decide/evolve pattern.
232- * Returns an AggregateResult with the updated aggregate and pending events.
233- */
234- fun handleUpdateTitle (title : String , now : Instant = Clock .System .now()): Either <ScopesError , AggregateResult <ScopeAggregate , ScopeEvent >> = either {
235- val pendingEvents = decideUpdateTitle(title, now).bind()
236-
237- if (pendingEvents.isEmpty()) {
238- return @either AggregateResult (
239- aggregate = this @ScopeAggregate,
240- events = emptyList(),
241- baseVersion = version,
242- )
243- }
244-
245- // Evolve phase - apply events to aggregate
246- val evolvedAggregate = pendingEvents.fold(this @ScopeAggregate) { agg, envelope ->
247- agg.applyEvent(envelope.event)
248- }
249-
250- AggregateResult (
251- aggregate = evolvedAggregate,
252- events = pendingEvents,
253- baseVersion = version,
254- )
255- }
256197
257198 /* *
258199 * Updates the scope description after validation.
@@ -272,7 +213,7 @@ data class ScopeAggregate(
272213 }
273214
274215 val event = ScopeDescriptionUpdated (
275- aggregateId = id ,
216+ aggregateId = aggregateId ,
276217 eventId = EventId .generate(),
277218 occurredAt = now,
278219
@@ -303,7 +244,7 @@ data class ScopeAggregate(
303244 }
304245
305246 val event = ScopeParentChanged (
306- aggregateId = id ,
247+ aggregateId = aggregateId ,
307248 eventId = EventId .generate(),
308249 occurredAt = now,
309250
@@ -330,7 +271,7 @@ data class ScopeAggregate(
330271 }
331272
332273 val event = ScopeDeleted (
333- aggregateId = id ,
274+ aggregateId = aggregateId ,
334275 eventId = EventId .generate(),
335276 occurredAt = now,
336277
@@ -358,7 +299,7 @@ data class ScopeAggregate(
358299 }
359300
360301 val event = ScopeArchived (
361- aggregateId = id ,
302+ aggregateId = aggregateId ,
362303 eventId = EventId .generate(),
363304 occurredAt = now,
364305
@@ -386,7 +327,7 @@ data class ScopeAggregate(
386327 }
387328
388329 val event = ScopeRestored (
389- aggregateId = id ,
330+ aggregateId = aggregateId ,
390331 eventId = EventId .generate(),
391332 occurredAt = now,
392333
@@ -405,7 +346,7 @@ data class ScopeAggregate(
405346 * We don't use event.aggregateVersion directly, but instead
406347 * increment based on the number of events applied.
407348 */
408- override fun applyEvent (event : ScopeEvent ): ScopeAggregate = when (event) {
349+ fun applyEvent (event : ScopeEvent ): ScopeAggregate = when (event) {
409350 is ScopeCreated -> copy(
410351 version = version.increment(),
411352 createdAt = event.occurredAt,
0 commit comments