diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ddd859..68cd2b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Added `@OperationTarget()` and `@OperationFor(...)` annotations in `continuum` to support operation-driven mutation without requiring `AggregateRoot`. + +### Changed + +- `continuum_generator` now discovers operation targets via `@OperationTarget()` (and still supports `AggregateRoot` as a legacy marker). +- `continuum_lints` now applies missing-handler and missing-creation-factory checks to `@OperationTarget()` classes. +- `EventSourcingStore` and `StateBasedStore` now accept `targets:` instead of `aggregates:` (with `aggregates:` kept as a deprecated alias). +- State-based persistence now prefers `TargetPersistenceAdapter`; `AggregatePersistenceAdapter` remains as a deprecated alias for backward compatibility. + +### Fixed + +- Example `abstract_interface_targets.dart` is now discoverable by codegen by marking its target types with `@OperationTarget()`. + +### Deprecated + +- Deprecated `@AggregateEvent(...)` in favor of operation-oriented annotations. + ## [5.1.0] - 2026-02-20 ### Added diff --git a/README.md b/README.md index 60cb268..d8efdaf 100644 --- a/README.md +++ b/README.md @@ -9,10 +9,10 @@ Continuum provides a comprehensive event sourcing framework for Dart application | Layer | Package | Purpose | |-------|---------|---------| | 0 | **`continuum`** | Core types: annotations, events, identity, dispatch registries | -| 0 | **`continuum_generator`** | Code generator for aggregate and event boilerplate | +| 0 | **`continuum_generator`** | Code generator for operation and event boilerplate | | 1 | **`continuum_uow`** | Unit of Work session engine: sessions, transactional runner, commit handler | | 2 | **`continuum_event_sourcing`** | Event sourcing persistence: event stores, serialization, projections | -| 2 | **`continuum_state`** | State-based persistence: REST/DB adapter-driven aggregate persistence | +| 2 | **`continuum_state`** | State-based persistence: REST/DB adapter-driven target persistence | | 3 | **`continuum_store_memory`** | In-memory EventStore for testing | | 3 | **`continuum_store_hive`** | Hive-backed EventStore for local persistence | | 3 | **`continuum_store_sembast`** | Sembast-backed EventStore for cross-platform persistence | @@ -70,10 +70,12 @@ import 'package:continuum/continuum.dart'; part 'shopping_cart.g.dart'; -class ShoppingCart extends AggregateRoot with _$ShoppingCartEventHandlers { +@OperationTarget() +class ShoppingCart with _$ShoppingCartEventHandlers { + String id; List items; - ShoppingCart._({required super.id, required this.items}); + ShoppingCart._({required this.id, required this.items}); static ShoppingCart createFromCartCreated(CartCreated event) { return ShoppingCart._(id: event.cartId, items: []); } @@ -84,7 +86,7 @@ class ShoppingCart extends AggregateRoot with _$ShoppingCartEventHandler } } -@AggregateEvent(of: ShoppingCart, type: 'cart.created', creation: true) +@OperationFor(type: ShoppingCart, key: 'cart.created', creation: true) class CartCreated implements ContinuumEvent { final String cartId; @@ -121,7 +123,7 @@ import 'continuum.g.dart'; final store = EventSourcingStore( eventStore: InMemoryEventStore(), - aggregates: $aggregateList, + targets: $aggregateList, ); final session = store.openSession(); @@ -144,7 +146,7 @@ import 'continuum.g.dart'; final store = StateBasedStore( adapters: {ShoppingCart: CartApiAdapter(httpClient)}, - aggregates: $aggregateList, + targets: $aggregateList, ); final session = store.openSession(); @@ -159,7 +161,7 @@ await session.saveChangesAsync(); // Adapter persists to backend ### continuum -Core library providing annotations (`@Aggregate`, `@AggregateEvent`, `@Projection`), event contracts (`ContinuumEvent`), identity types (`EventId`, `StreamId`), dispatch registries, `EventApplicationMode`, and core exceptions. +Core library providing annotations (`@OperationTarget`, `@OperationFor`, `@Projection`), event contracts (`ContinuumEvent`), identity types (`EventId`, `StreamId`), dispatch registries, `EventApplicationMode`, and core exceptions. ### continuum_generator @@ -191,7 +193,7 @@ Sembast-backed `EventStore` implementation for cross-platform local persistence. ### continuum_lints -Custom lint rules for `@Aggregate` and `@Projection` classes. +Custom lint rules for operation targets and projections. ## License diff --git a/doc/draft/extract-unit-of-work-package.md b/doc/draft/extract-unit-of-work-package.md index 532f56a..80cf0e2 100644 --- a/doc/draft/extract-unit-of-work-package.md +++ b/doc/draft/extract-unit-of-work-package.md @@ -41,7 +41,7 @@ Layer 3 ─ Store implementations | Layer | Package | Contains | |---|---|---| -| 0 | `continuum` | `Operation`, `ContinuumEvent`, `@AggregateEvent`, `@Projection`, `AggregateFactoryRegistry`, `EventApplierRegistry`, `EventApplicationMode`, `StreamId`, `GeneratedAggregate`, codegen annotations, all operation/event base contracts | +| 0 | `continuum` | `Operation`, `ContinuumEvent`, `@OperationTarget`, `@OperationFor`, `@Projection`, `AggregateFactoryRegistry`, `EventApplierRegistry`, `EventApplicationMode`, `StreamId`, `GeneratedAggregate`, codegen annotations, all operation/event base contracts | | 1 | `continuum_uow` | `Session`, `SessionBase`, `TrackedEntity`, `SessionStore`, `TransactionalRunner`, `CommitHandler`, `ConcurrencyException`, `InvalidOperationException`, `UnsupportedOperationException`, `PartialSaveException` | | 2a | `continuum_es` | `EventSourcingStore`, `SessionImpl`, `EventStore`, `AtomicEventStore`, `EventSerializer`, `EventSerializerRegistry`, `JsonEventSerializer`, `StoredEvent`, `ExpectedVersion`, `StreamAppendBatch`, `ProjectionEventStore`, all projection types | | 2b | `continuum_state` | `StateBasedStore`, `StateBasedSession`, `AggregatePersistenceAdapter`, `PermanentAdapterException`, `TransientAdapterException` | @@ -454,7 +454,7 @@ These are Continuum core — the mutation framework primitives: - `Operation` marker interface - `ContinuumEvent` (`implements Operation` + `BoundedDomainEvent` integration) -- `@AggregateEvent`, `@Projection` annotations +- `@OperationTarget`, `@OperationFor`, `@Projection` annotations - `AggregateFactoryRegistry`, `EventApplierRegistry` - `EventApplicationMode` - `StreamId` diff --git a/doc/draft/session-store-architecture.md b/doc/draft/session-store-architecture.md index bf7ca9a..74f7ae0 100644 --- a/doc/draft/session-store-architecture.md +++ b/doc/draft/session-store-architecture.md @@ -224,14 +224,14 @@ This is a **store-level or session-level configuration**, not a per-event decisi // Eager (default) — events mutate the aggregate immediately final store = EventSourcingStore( eventStore: SembastEventStore(database), - aggregates: [$User, $Playlist], + targets: [$User, $Playlist], applicationMode: EventApplicationMode.eager, ); // Deferred — events are recorded but applied only on successful save final store = EventSourcingStore( eventStore: SembastEventStore(database), - aggregates: [$User, $Playlist], + targets: [$User, $Playlist], applicationMode: EventApplicationMode.deferred, ); ``` @@ -1064,7 +1064,7 @@ State-Based: applyAsync → save to Backend → commitHandler → proj // main.dart final store = EventSourcingStore( eventStore: SembastEventStore(database), - aggregates: [$AudioFile, $Playlist], + targets: [$AudioFile, $Playlist], ); final runner = TransactionalRunner( diff --git a/packages/continuum/README.md b/packages/continuum/README.md index e3bb8bf..35d1104 100644 --- a/packages/continuum/README.md +++ b/packages/continuum/README.md @@ -19,7 +19,7 @@ dev_dependencies: ## What This Package Provides -- **Annotations**: `@AggregateEvent()`, `@Projection()` +- **Annotations**: `@OperationTarget()`, `@OperationFor(...)`, `@Projection()` - **Event contract**: `ContinuumEvent` interface - **Identity types**: `EventId`, `StreamId` - **Operation enum**: `Operation.create`, `Operation.mutate` @@ -48,18 +48,20 @@ Continuum is organized into four layers: ## Quick Start -### Define Your Aggregate +### Define Your Target ```dart import 'package:continuum/continuum.dart'; part 'user.g.dart'; -class User extends AggregateRoot with _$UserEventHandlers { +@OperationTarget() +class User with _$UserEventHandlers { + String id; String name; String email; - User._({required super.id, required this.name, required this.email}); + User._({required this.id, required this.name, required this.email}); static User createFromUserRegistered(UserRegistered event) { return User._(id: event.userId, name: event.name, email: event.email); @@ -77,7 +79,7 @@ class User extends AggregateRoot with _$UserEventHandlers { ```dart import 'package:continuum/continuum.dart'; -@AggregateEvent(of: User, type: 'user.registered', creation: true) +@OperationFor(type: User, key: 'user.registered', creation: true) class UserRegistered implements ContinuumEvent { UserRegistered({ required this.userId, @@ -111,7 +113,7 @@ dart run build_runner build This creates: - `user.g.dart` with `_$UserEventHandlers` mixin -- `lib/continuum.g.dart` with `$aggregateList` +- `lib/continuum.g.dart` with `$aggregateList` (auto-discovered operation targets) ## Custom Lints (Recommended) diff --git a/packages/continuum/example/lib/abstract_interface_aggregates.dart b/packages/continuum/example/lib/abstract_interface_targets.dart similarity index 80% rename from packages/continuum/example/lib/abstract_interface_aggregates.dart rename to packages/continuum/example/lib/abstract_interface_targets.dart index 52a0919..5c1267e 100644 --- a/packages/continuum/example/lib/abstract_interface_aggregates.dart +++ b/packages/continuum/example/lib/abstract_interface_targets.dart @@ -1,28 +1,28 @@ -/// Example: Abstract and Interface Aggregates +/// Example: Abstract and Interface Targets /// /// This example demonstrates that Continuum can generate event handlers and -/// dispatch logic for aggregates declared as an `abstract class` or an +/// dispatch logic for targets declared as an `abstract class` or an /// `interface class`. library; import 'package:bounded/bounded.dart'; import 'package:continuum/continuum.dart'; -part 'abstract_interface_aggregates.g.dart'; +part 'abstract_interface_targets.g.dart'; void main() { print('═══════════════════════════════════════════════════════════════════'); - print('Example: Abstract and Interface Aggregates'); + print('Example: Abstract and Interface Targets'); print('═══════════════════════════════════════════════════════════════════'); print(''); - _runAbstractAggregateExample(); + _runAbstractTargetExample(); print(''); - _runInterfaceAggregateExample(); + _runInterfaceTargetExample(); } -void _runAbstractAggregateExample() { - print('ABSTRACT AGGREGATE'); +void _runAbstractTargetExample() { + print('ABSTRACT TARGET'); final user = AbstractUser( id: const AbstractUserId('abstract-user-1'), @@ -42,8 +42,8 @@ void _runAbstractAggregateExample() { print(' ✓ Event dispatch works via AbstractUserBase'); } -void _runInterfaceAggregateExample() { - print('CONCRETE AGGREGATE'); +void _runInterfaceTargetExample() { + print('CONCRETE TARGET'); final user = UserContract( id: const UserContractId('contract-user-1'), @@ -66,23 +66,25 @@ final class AbstractUserId extends TypedIdentity { const AbstractUserId(super.value); } -/// An abstract aggregate base type. +/// An abstract target base type. /// /// The generator produces: /// - `mixin _$AbstractUserBaseEventHandlers` /// - `extension $AbstractUserBaseEventDispatch on AbstractUserBase` -abstract class AbstractUserBase extends AggregateRoot with _$AbstractUserBaseEventHandlers { +@OperationTarget() +abstract class AbstractUserBase with _$AbstractUserBaseEventHandlers { AbstractUserBase({ - required AbstractUserId id, + required this.id, required this.email, required this.name, - }) : super(id); + }); + final AbstractUserId id; String email; final String name; } -/// A concrete implementation of the abstract aggregate. +/// A concrete implementation of the abstract target. class AbstractUser extends AbstractUserBase { AbstractUser({ required super.id, @@ -100,12 +102,15 @@ final class UserContractId extends TypedIdentity { const UserContractId(super.value); } -/// A concrete implementation of the interface aggregate. -class UserContract extends AggregateRoot with _$UserContractEventHandlers { +/// A concrete implementation of the interface target. +@OperationTarget() +class UserContract with _$UserContractEventHandlers { UserContract({ - required UserContractId id, + required this.id, required this.displayName, - }) : super(id); + }); + + final UserContractId id; String displayName; @@ -116,7 +121,7 @@ class UserContract extends AggregateRoot with _$UserContractEven } /// Event that changes an abstract user's email. -@AggregateEvent(of: AbstractUserBase, type: 'example.abstract_user.email_changed') +@OperationFor(type: AbstractUserBase, key: 'example.abstract_user.email_changed') class AbstractUserEmailChanged implements ContinuumEvent { AbstractUserEmailChanged({ required this.newEmail, @@ -155,8 +160,8 @@ class AbstractUserEmailChanged implements ContinuumEvent { }; } -/// Event that renames a user implementing an interface aggregate. -@AggregateEvent(of: UserContract, type: 'example.contract_user.renamed') +/// Event that renames a user implementing an interface target. +@OperationFor(type: UserContract, key: 'example.contract_user.renamed') class ContractUserRenamed implements ContinuumEvent { ContractUserRenamed({ required this.newDisplayName, diff --git a/packages/continuum/example/lib/abstract_interface_aggregates.g.dart b/packages/continuum/example/lib/abstract_interface_targets.g.dart similarity index 70% rename from packages/continuum/example/lib/abstract_interface_aggregates.g.dart rename to packages/continuum/example/lib/abstract_interface_targets.g.dart index fa24b63..1f92389 100644 --- a/packages/continuum/example/lib/abstract_interface_aggregates.g.dart +++ b/packages/continuum/example/lib/abstract_interface_targets.g.dart @@ -1,6 +1,6 @@ // GENERATED CODE - DO NOT MODIFY BY HAND -part of 'abstract_interface_aggregates.dart'; +part of 'abstract_interface_targets.dart'; // ************************************************************************** // ContinuumGenerator @@ -64,21 +64,36 @@ extension $AbstractUserBaseCreation on Never { /// Contains all serializers, factories, and appliers for this aggregate. /// Add to the `aggregates` list when creating an [EventSourcingStore]. final $AbstractUserBase = GeneratedAggregate( - serializerRegistry: EventSerializerRegistry({ - AbstractUserEmailChanged: EventSerializerEntry( - eventType: 'example.abstract_user.email_changed', - toJson: (event) => (event as AbstractUserEmailChanged).toJson(), - fromJson: AbstractUserEmailChanged.fromJson, - ), - }), - aggregateFactories: AggregateFactoryRegistry({}), - eventAppliers: EventApplierRegistry({ - AbstractUserBase: { - AbstractUserEmailChanged: (aggregate, event) => (aggregate as AbstractUserBase).applyAbstractUserEmailChanged( - event as AbstractUserEmailChanged, + serializerRegistry: EventSerializerRegistry( + { + AbstractUserEmailChanged: EventSerializerEntry( + eventType: 'example.abstract_user.email_changed', + toJson: (event) => (event as AbstractUserEmailChanged).toJson(), + fromJson: AbstractUserEmailChanged.fromJson, ), }, - }), + matchers: { + AbstractUserEmailChanged: (Object event) => + event is AbstractUserEmailChanged, + }, + ), + aggregateFactories: AggregateFactoryRegistry({}, matchers: {}), + eventAppliers: EventApplierRegistry( + { + AbstractUserBase: { + AbstractUserEmailChanged: (aggregate, event) => + (aggregate as AbstractUserBase).applyAbstractUserEmailChanged( + event as AbstractUserEmailChanged, + ), + }, + }, + matchers: { + AbstractUserBase: { + AbstractUserEmailChanged: (Object event) => + event is AbstractUserEmailChanged, + }, + }, + ), ); /// Generated mixin requiring apply methods for UserContract mutation events. @@ -139,17 +154,30 @@ extension $UserContractCreation on Never { /// Contains all serializers, factories, and appliers for this aggregate. /// Add to the `aggregates` list when creating an [EventSourcingStore]. final $UserContract = GeneratedAggregate( - serializerRegistry: EventSerializerRegistry({ - ContractUserRenamed: EventSerializerEntry( - eventType: 'example.contract_user.renamed', - toJson: (event) => (event as ContractUserRenamed).toJson(), - fromJson: ContractUserRenamed.fromJson, - ), - }), - aggregateFactories: AggregateFactoryRegistry({}), - eventAppliers: EventApplierRegistry({ - UserContract: { - ContractUserRenamed: (aggregate, event) => (aggregate as UserContract).applyContractUserRenamed(event as ContractUserRenamed), + serializerRegistry: EventSerializerRegistry( + { + ContractUserRenamed: EventSerializerEntry( + eventType: 'example.contract_user.renamed', + toJson: (event) => (event as ContractUserRenamed).toJson(), + fromJson: ContractUserRenamed.fromJson, + ), + }, + matchers: { + ContractUserRenamed: (Object event) => event is ContractUserRenamed, + }, + ), + aggregateFactories: AggregateFactoryRegistry({}, matchers: {}), + eventAppliers: EventApplierRegistry( + { + UserContract: { + ContractUserRenamed: (aggregate, event) => (aggregate as UserContract) + .applyContractUserRenamed(event as ContractUserRenamed), + }, + }, + matchers: { + UserContract: { + ContractUserRenamed: (Object event) => event is ContractUserRenamed, + }, }, - }), + ), ); diff --git a/packages/continuum/example/lib/continuum.g.dart b/packages/continuum/example/lib/continuum.g.dart index ab6ff3a..83ac7c7 100644 --- a/packages/continuum/example/lib/continuum.g.dart +++ b/packages/continuum/example/lib/continuum.g.dart @@ -3,10 +3,9 @@ // ignore_for_file: type=lint import 'package:continuum/continuum.dart'; -import 'package:continuum_event_sourcing/continuum_event_sourcing.dart'; import 'package:continuum_uow/continuum_uow.dart'; -import 'abstract_interface_aggregates.dart'; +import 'abstract_interface_targets.dart'; import 'domain/projections/user_profile_projection.dart'; import 'domain/user.dart'; @@ -23,7 +22,7 @@ final List $aggregateList = [ /// All discovered projections in this package. /// /// Use this list to register all projections with the registry, -/// or use the generated [$ProjectionRegistryExtensions.registerAll] method. +/// or use the generated [ProjectionRegistryExtensions.registerAll] method. final List $projectionList = [ $UserProfileProjection, ]; diff --git a/packages/continuum/example/lib/domain/events/email_changed.dart b/packages/continuum/example/lib/domain/events/email_changed.dart index b51c506..727280d 100644 --- a/packages/continuum/example/lib/domain/events/email_changed.dart +++ b/packages/continuum/example/lib/domain/events/email_changed.dart @@ -3,7 +3,7 @@ import 'package:continuum/continuum.dart'; import '../user.dart'; /// Event fired when a user changes their email address. -@AggregateEvent(of: User, type: 'user.email_changed') +@OperationFor(type: User, key: 'user.email_changed') class EmailChanged implements ContinuumEvent { EmailChanged({ required this.newEmail, @@ -17,11 +17,11 @@ class EmailChanged implements ContinuumEvent { /// The user this event belongs to. /// - /// Optional because the aggregate context provides the stream ID + /// Optional because the target context provides the stream ID /// during event sourcing. Required for projection key extraction - /// when events are processed outside the aggregate context. + /// when events are processed outside the target context. final UserId? userId; - + final String newEmail; @override diff --git a/packages/continuum/example/lib/domain/events/user_deactivated.dart b/packages/continuum/example/lib/domain/events/user_deactivated.dart index d5e764d..11db3ce 100644 --- a/packages/continuum/example/lib/domain/events/user_deactivated.dart +++ b/packages/continuum/example/lib/domain/events/user_deactivated.dart @@ -3,7 +3,7 @@ import 'package:continuum/continuum.dart'; import '../user.dart'; /// Event fired when a user account is deactivated. -@AggregateEvent(of: User, type: 'user.deactivated') +@OperationFor(type: User, key: 'user.deactivated') class UserDeactivated implements ContinuumEvent { UserDeactivated({ required this.deactivatedAt, @@ -18,9 +18,9 @@ class UserDeactivated implements ContinuumEvent { /// The user this event belongs to. /// - /// Optional because the aggregate context provides the stream ID + /// Optional because the target context provides the stream ID /// during event sourcing. Required for projection key extraction - /// when events are processed outside the aggregate context. + /// when events are processed outside the target context. final UserId? userId; final DateTime deactivatedAt; final String? reason; diff --git a/packages/continuum/example/lib/domain/events/user_registered.dart b/packages/continuum/example/lib/domain/events/user_registered.dart index d5ea79b..5313d92 100644 --- a/packages/continuum/example/lib/domain/events/user_registered.dart +++ b/packages/continuum/example/lib/domain/events/user_registered.dart @@ -3,7 +3,7 @@ import 'package:continuum/continuum.dart'; import '../user.dart'; /// Event fired when a new user registers. -@AggregateEvent(of: User, type: 'user.registered', creation: true) +@OperationFor(type: User, key: 'user.registered', creation: true) final class UserRegistered implements ContinuumEvent { UserRegistered({ required this.userId, diff --git a/packages/continuum/example/lib/domain/projections/user_profile_projection.dart b/packages/continuum/example/lib/domain/projections/user_profile_projection.dart index 0e7e374..a406864 100644 --- a/packages/continuum/example/lib/domain/projections/user_profile_projection.dart +++ b/packages/continuum/example/lib/domain/projections/user_profile_projection.dart @@ -9,7 +9,7 @@ part 'user_profile_projection.g.dart'; /// Read model for a user's profile information. /// /// This is a denormalized view optimized for querying user profile data -/// without reconstructing the full aggregate. +/// without reconstructing the full target. class UserProfile { final String name; final String email; diff --git a/packages/continuum/example/lib/domain/projections/user_profile_projection.g.dart b/packages/continuum/example/lib/domain/projections/user_profile_projection.g.dart index 899d96d..7321bae 100644 --- a/packages/continuum/example/lib/domain/projections/user_profile_projection.g.dart +++ b/packages/continuum/example/lib/domain/projections/user_profile_projection.g.dart @@ -47,7 +47,7 @@ mixin _$UserProfileProjectionHandlers { UserProfile applyUserDeactivated(UserProfile current, UserDeactivated event); } -/// Generated extension providing additional event dispatch for UserProfileProjection. +/// Generated extension providing additional operation dispatch for UserProfileProjection. extension $UserProfileProjectionEventDispatch on UserProfileProjection { /// Routes an operation to the appropriate apply method. /// diff --git a/packages/continuum/example/lib/domain/user.dart b/packages/continuum/example/lib/domain/user.dart index 534ded2..6d15059 100644 --- a/packages/continuum/example/lib/domain/user.dart +++ b/packages/continuum/example/lib/domain/user.dart @@ -11,10 +11,11 @@ final class UserId extends TypedIdentity { const UserId(super.value); } -/// A User aggregate demonstrating event sourcing. +/// A User operation target demonstrating event sourcing. /// /// Users are created via registration, can update their email, /// and can be deactivated. Each state change is an event. +@OperationTarget() class User extends AggregateRoot with _$UserEventHandlers { String email; String name; diff --git a/packages/continuum/example/lib/domain/user.g.dart b/packages/continuum/example/lib/domain/user.g.dart index ba9d61f..2cacea3 100644 --- a/packages/continuum/example/lib/domain/user.g.dart +++ b/packages/continuum/example/lib/domain/user.g.dart @@ -71,32 +71,55 @@ extension $UserCreation on Never { /// Contains all serializers, factories, and appliers for this aggregate. /// Add to the `aggregates` list when creating an [EventSourcingStore]. final $User = GeneratedAggregate( - serializerRegistry: EventSerializerRegistry({ - UserRegistered: EventSerializerEntry( - eventType: 'user.registered', - toJson: (event) => (event as UserRegistered).toJson(), - fromJson: UserRegistered.fromJson, - ), - EmailChanged: EventSerializerEntry( - eventType: 'user.email_changed', - toJson: (event) => (event as EmailChanged).toJson(), - fromJson: EmailChanged.fromJson, - ), - UserDeactivated: EventSerializerEntry( - eventType: 'user.deactivated', - toJson: (event) => (event as UserDeactivated).toJson(), - fromJson: UserDeactivated.fromJson, - ), - }), - aggregateFactories: AggregateFactoryRegistry({ - User: { - UserRegistered: (event) => User.createFromUserRegistered(event as UserRegistered), + serializerRegistry: EventSerializerRegistry( + { + UserRegistered: EventSerializerEntry( + eventType: 'user.registered', + toJson: (event) => (event as UserRegistered).toJson(), + fromJson: UserRegistered.fromJson, + ), + EmailChanged: EventSerializerEntry( + eventType: 'user.email_changed', + toJson: (event) => (event as EmailChanged).toJson(), + fromJson: EmailChanged.fromJson, + ), + UserDeactivated: EventSerializerEntry( + eventType: 'user.deactivated', + toJson: (event) => (event as UserDeactivated).toJson(), + fromJson: UserDeactivated.fromJson, + ), }, - }), - eventAppliers: EventApplierRegistry({ - User: { - EmailChanged: (aggregate, event) => (aggregate as User).applyEmailChanged(event as EmailChanged), - UserDeactivated: (aggregate, event) => (aggregate as User).applyUserDeactivated(event as UserDeactivated), + matchers: { + UserRegistered: (Object event) => event is UserRegistered, + EmailChanged: (Object event) => event is EmailChanged, + UserDeactivated: (Object event) => event is UserDeactivated, }, - }), + ), + aggregateFactories: AggregateFactoryRegistry( + { + User: { + UserRegistered: (event) => + User.createFromUserRegistered(event as UserRegistered), + }, + }, + matchers: { + User: {UserRegistered: (Object event) => event is UserRegistered}, + }, + ), + eventAppliers: EventApplierRegistry( + { + User: { + EmailChanged: (aggregate, event) => + (aggregate as User).applyEmailChanged(event as EmailChanged), + UserDeactivated: (aggregate, event) => + (aggregate as User).applyUserDeactivated(event as UserDeactivated), + }, + }, + matchers: { + User: { + EmailChanged: (Object event) => event is EmailChanged, + UserDeactivated: (Object event) => event is UserDeactivated, + }, + }, + ), ); diff --git a/packages/continuum/example/lib/event_replay.dart b/packages/continuum/example/lib/event_replay.dart index ff00899..00e62e2 100644 --- a/packages/continuum/example/lib/event_replay.dart +++ b/packages/continuum/example/lib/event_replay.dart @@ -3,8 +3,8 @@ /// Example 3: Rebuilding State by Replaying Events /// /// This example demonstrates event sourcing's core principle: rebuilding -/// aggregate state by replaying its event history. This is how event stores -/// load aggregates from persistence. +/// target state by replaying its event history. This is how event stores +/// load targets from persistence. library; import 'package:continuum/continuum.dart'; @@ -44,7 +44,7 @@ void main() { } print(''); - // Rebuild the aggregate by replaying events + // Rebuild the target by replaying events print('Replaying events to rebuild state...'); final creationEvent = events.first as UserRegistered; final user = User.createFromUserRegistered(creationEvent); @@ -53,7 +53,7 @@ void main() { user.replayEvents(events.skip(1)); print(''); - print('Rebuilt aggregate state:'); + print('Rebuilt target state:'); print(' $user'); print(''); diff --git a/packages/continuum/example/lib/projection_example.dart b/packages/continuum/example/lib/projection_example.dart index fe17964..9889ab6 100644 --- a/packages/continuum/example/lib/projection_example.dart +++ b/packages/continuum/example/lib/projection_example.dart @@ -34,7 +34,7 @@ void main() async { // Create event sourcing store. final store = EventSourcingStore( eventStore: InMemoryEventStore(), - aggregates: $aggregateList, + targets: $aggregateList, ); // Wire everything together with TransactionalRunner. diff --git a/packages/continuum/example/lib/store_atomic_rollback.dart b/packages/continuum/example/lib/store_atomic_rollback.dart index d5d3cc1..fb0aae1 100644 --- a/packages/continuum/example/lib/store_atomic_rollback.dart +++ b/packages/continuum/example/lib/store_atomic_rollback.dart @@ -35,7 +35,7 @@ void main() async { final store = EventSourcingStore( eventStore: InMemoryEventStore(), - aggregates: $aggregateList, + targets: $aggregateList, ); // Setup: Create two users @@ -127,5 +127,5 @@ void main() async { print(''); print('✓ Atomic rollback prevented partial write!'); print(' Even though Bob had no conflict, it was not persisted.'); - print(' This preserves cross-aggregate consistency.'); + print(' This preserves cross-target consistency.'); } diff --git a/packages/continuum/example/lib/store_atomic_saves.dart b/packages/continuum/example/lib/store_atomic_saves.dart index c7338d6..1637b22 100644 --- a/packages/continuum/example/lib/store_atomic_saves.dart +++ b/packages/continuum/example/lib/store_atomic_saves.dart @@ -1,11 +1,11 @@ /// Store Example: Atomic Multi-Stream Saves /// -/// Demonstrates that a single session can modify multiple aggregates and persist +/// Demonstrates that a single session can modify multiple targets and persist /// all changes atomically - either all succeed or all fail together. /// /// What you'll learn: /// - How one saveChangesAsync() can persist multiple streams -/// - Why this matters for maintaining consistency across related aggregates +/// - Why this matters for maintaining consistency across related targets /// - That stores implementing AtomicEventStore support this /// /// Real-world use cases: @@ -35,7 +35,7 @@ void main() async { final store = EventSourcingStore( eventStore: InMemoryEventStore(), - aggregates: $aggregateList, + targets: $aggregateList, ); // Setup: Create two users @@ -67,7 +67,7 @@ void main() async { print('Updating both users in one transaction...'); print(''); - // Open one session for both aggregates + // Open one session for both targets session = store.openSession(); // Load both users diff --git a/packages/continuum/example/lib/store_creating_streams.dart b/packages/continuum/example/lib/store_creating_streams.dart index 4f841f5..364e976 100644 --- a/packages/continuum/example/lib/store_creating_streams.dart +++ b/packages/continuum/example/lib/store_creating_streams.dart @@ -1,6 +1,6 @@ /// Store Example: Creating Streams /// -/// Demonstrates the fundamental operation: creating a new aggregate and persisting +/// Demonstrates the fundamental operation: creating a new target and persisting /// its creation event to the event store. /// /// What you'll learn: @@ -26,13 +26,13 @@ void main() async { final store = EventSourcingStore( eventStore: InMemoryEventStore(), - aggregates: $aggregateList, + targets: $aggregateList, ); print('Creating a new user stream...'); print(''); - // Every aggregate lives in its own stream, identified by a StreamId + // Every target lives in its own stream, identified by a StreamId final userId = const StreamId('user-001'); // Step 1: Open a session @@ -40,8 +40,8 @@ void main() async { final session = store.openSession(); // Step 2: Apply a creation event to start a new stream - // applyAsync() detects this is a creation event and creates the aggregate - // The aggregate is now in memory and tracked by the session + // applyAsync() detects this is a creation event and creates the target + // The target is now in memory and tracked by the session print(' [Session] Starting new stream...'); final user = await session.applyAsync( userId, @@ -51,7 +51,7 @@ void main() async { name: 'Alice Smith', ), ); - print(' [Memory] Aggregate created: $user'); + print(' [Memory] Target created: $user'); print(''); // Step 3: Save changes @@ -64,5 +64,5 @@ void main() async { print('✓ The UserRegistered event is now in the event store.'); print(' Stream ID: ${userId.value}'); - print(' You can now reload this aggregate in a future session.'); + print(' You can now reload this target in a future session.'); } diff --git a/packages/continuum/example/lib/store_handling_conflicts.dart b/packages/continuum/example/lib/store_handling_conflicts.dart index b279910..818855d 100644 --- a/packages/continuum/example/lib/store_handling_conflicts.dart +++ b/packages/continuum/example/lib/store_handling_conflicts.dart @@ -1,7 +1,7 @@ /// Store Example: Handling Concurrency Conflicts /// /// Demonstrates optimistic concurrency control: when two sessions try to save -/// changes to the same aggregate, the second one detects that the aggregate +/// changes to the same target, the second one detects that the target /// has changed and throws ConcurrencyException. /// /// What you'll learn: @@ -34,7 +34,7 @@ void main() async { final store = EventSourcingStore( eventStore: InMemoryEventStore(), - aggregates: $aggregateList, + targets: $aggregateList, ); // Setup: Create a user @@ -53,7 +53,7 @@ void main() async { print(''); // Simulate two concurrent operations - print('Simulating two users editing the same aggregate simultaneously...'); + print('Simulating two users editing the same target simultaneously...'); print(''); // Session 1: Admin loads the user diff --git a/packages/continuum/example/lib/store_loading_and_updating.dart b/packages/continuum/example/lib/store_loading_and_updating.dart index 2a51c5a..1a45f70 100644 --- a/packages/continuum/example/lib/store_loading_and_updating.dart +++ b/packages/continuum/example/lib/store_loading_and_updating.dart @@ -1,11 +1,11 @@ /// Store Example: Loading and Updating /// -/// Demonstrates the typical workflow: load an existing aggregate from the store, +/// Demonstrates the typical workflow: load an existing target from the store, /// apply changes via events, and persist those changes. /// /// What you'll learn: -/// - How loadAsync() rebuilds aggregates by replaying their event history -/// - How to apply mutation events to an aggregate with applyAsync() +/// - How loadAsync() rebuilds targets by replaying their event history +/// - How to apply mutation events to a target with applyAsync() /// - Why each session is independent (fresh load every time) /// /// Real-world use case: Editing profiles, updating orders, processing transactions @@ -28,7 +28,7 @@ void main() async { final store = EventSourcingStore( eventStore: InMemoryEventStore(), - aggregates: $aggregateList, + targets: $aggregateList, ); // Setup: Create a user first @@ -54,17 +54,17 @@ void main() async { // Sessions are short-lived - open one per logical operation session = store.openSession(); - // Step 2: Load the aggregate from the stream + // Step 2: Load the target from the stream // loadAsync() fetches ALL events for this stream and replays them - // to rebuild the current aggregate state + // to rebuild the current target state print(' [Store] Loading events from stream ${userId.value}...'); print(' [Memory] Replaying events to rebuild state...'); final user = await session.loadAsync(userId); - print(' [Memory] Aggregate loaded: $user'); + print(' [Memory] Target loaded: $user'); print(''); // Step 3: Apply events to mutate state - // applyAsync() applies the event to the in-memory aggregate + // applyAsync() applies the event to the in-memory target // and tracks it for persistence print(' [Session] Applying EmailChanged event...'); await session.applyAsync( diff --git a/packages/continuum/example/lib/store_state_based.dart b/packages/continuum/example/lib/store_state_based.dart index 8951e13..2b92ae5 100644 --- a/packages/continuum/example/lib/store_state_based.dart +++ b/packages/continuum/example/lib/store_state_based.dart @@ -2,15 +2,15 @@ /// /// Demonstrates `StateBasedStore` — the persistence mode for apps backed /// by a traditional backend (REST API, GraphQL, database). Instead of -/// persisting events, each aggregate is loaded and saved through an -/// `AggregatePersistenceAdapter` that talks to the backend. +/// persisting events, each target is loaded and saved through an +/// [TargetPersistenceAdapter] that talks to the backend. /// /// What you'll learn: -/// - How to implement `AggregatePersistenceAdapter` for your aggregate -/// - How to construct a `StateBasedStore` from adapters and aggregate metadata +/// - How to implement [TargetPersistenceAdapter] for your target +/// - How to construct a `StateBasedStore` from adapters and target metadata /// - How sessions work identically to `EventSourcingStore` (same `applyAsync` / /// `saveChangesAsync` contract) -/// - How the adapter receives the post-event aggregate state on save +/// - How the adapter receives the post-event target state on save /// /// Real-world use case: Backend-authoritative apps where the frontend uses /// domain events for local state management, then syncs to a REST API. @@ -27,9 +27,9 @@ import 'package:continuum_uow/continuum_uow.dart'; /// A fake backend adapter that stores user state in memory. /// /// In a real app this would make HTTP requests to your backend API. -/// The adapter translates between the domain aggregate and the backend's +/// The adapter translates between the domain target and the backend's /// representation (DTOs, JSON, etc.). -class FakeUserApiAdapter implements AggregatePersistenceAdapter { +class FakeUserApiAdapter implements TargetPersistenceAdapter { /// Simulates a backend database keyed by stream ID. final Map _backendDb = {}; @@ -45,7 +45,7 @@ class FakeUserApiAdapter implements AggregatePersistenceAdapter { print(' [Backend] GET /users/${streamId.value} → 200 OK'); - // Reconstruct the domain aggregate from the stored record. + // Reconstruct the domain target from the stored record. return User.createFromUserRegistered( UserRegistered( userId: UserId(record.id), @@ -58,18 +58,18 @@ class FakeUserApiAdapter implements AggregatePersistenceAdapter { @override Future persistAsync( StreamId streamId, - User aggregate, + User target, List pendingOperations, ) async { // Simulate network latency. await Future.delayed(const Duration(milliseconds: 50)); - // The adapter decides how to translate the aggregate + events into + // The adapter decides how to translate the target + events into // backend calls. Here we simply store the final state. _backendDb[streamId.value] = _UserRecord( id: streamId.value, - email: aggregate.email, - name: aggregate.name, + email: target.email, + name: target.name, ); print( @@ -98,15 +98,15 @@ void main() async { print('═══════════════════════════════════════════════════════════════════'); print(''); - // The adapter bridges your aggregate to the backend. + // The adapter bridges your target to the backend. // In production you'd inject an HTTP client here. final userAdapter = FakeUserApiAdapter(); - // Construct the store with one adapter per aggregate type. + // Construct the store with one adapter per target type. // $aggregateList provides the generated event-application registries. final store = StateBasedStore( adapters: {User: userAdapter}, - aggregates: $aggregateList, + targets: $aggregateList, ); final userId = const StreamId('user-001'); @@ -140,7 +140,7 @@ void main() async { print(' [Memory] Updated: ${user.name} <${user.email}>'); print(''); - // Save — the adapter receives the post-event aggregate + pending events + // Save — the adapter receives the post-event target + pending events print(' [Persisting] Saving via adapter...'); await session.saveChangesAsync(); print(' Done.'); diff --git a/packages/continuum/example/lib/store_state_based_local_db.dart b/packages/continuum/example/lib/store_state_based_local_db.dart index 18a24ed..ba96906 100644 --- a/packages/continuum/example/lib/store_state_based_local_db.dart +++ b/packages/continuum/example/lib/store_state_based_local_db.dart @@ -1,19 +1,19 @@ /// State-Based Persistence with Local Database /// /// Demonstrates how to use `StateBasedStore` with a local database. -/// The adapter stores and loads aggregates as whole entities — no event +/// The adapter stores and loads targets as whole entities — no event /// sourcing, no remote backend. This is the simplest persistence model: /// load the full object, mutate it with domain events, and write it back. /// /// What you'll learn: -/// - How to implement `AggregatePersistenceAdapter` for a local database -/// - How the adapter serializes/deserializes whole aggregates (JSON maps) +/// - How to implement `TargetPersistenceAdapter` for a local database +/// - How the adapter serializes/deserializes whole targets (JSON maps) /// - How `TransactionalRunner` auto-commits changes to the local DB /// - How create vs. update is handled inside `persistAsync` /// /// Real-world use case: A mobile app that stores user profiles in a local /// database (Hive, Sembast, Isar, SQLite, etc.) with no backend involved. -/// The database holds the full current state of each aggregate. +/// The database holds the full current state of each target. library; import 'dart:convert'; @@ -75,17 +75,17 @@ final class FakeLocalDatabase { // ── Adapter ───────────────────────────────────────────────────────────────── -/// Bridges the `User` aggregate to a local key-value database. +/// Bridges the `User` target to a local key-value database. /// /// `fetchAsync` reads a JSON map from the database and reconstructs the -/// aggregate. `persistAsync` serializes the aggregate's current state +/// target. `persistAsync` serializes the target's current state /// and writes it back. The pending operations list is ignored here — we /// simply store the whole entity every time. /// /// This is the simplest adapter strategy: full-entity read/write. More /// sophisticated adapters could diff fields, use SQL UPDATE for changed /// columns only, etc. -final class UserLocalDbAdapter implements AggregatePersistenceAdapter { +final class UserLocalDbAdapter implements TargetPersistenceAdapter { /// The local database instance. final FakeLocalDatabase _db; @@ -99,7 +99,7 @@ final class UserLocalDbAdapter implements AggregatePersistenceAdapter { throw StateError('User not found in local DB: ${streamId.value}'); } - // Deserialize from JSON map → domain aggregate. + // Deserialize from JSON map → domain target. // First create via the creation factory, then restore mutable fields // that may have changed since creation. final user = User.createFromUserRegistered( @@ -122,17 +122,17 @@ final class UserLocalDbAdapter implements AggregatePersistenceAdapter { @override Future persistAsync( StreamId streamId, - User aggregate, + User target, List pendingOperations, ) async { - // Serialize the full aggregate state to a JSON map and write it. + // Serialize the full target state to a JSON map and write it. // We ignore pendingOperations entirely — just store the whole entity. await _db.put(streamId.value, { - 'id': aggregate.id.value, - 'name': aggregate.name, - 'email': aggregate.email, - 'isActive': aggregate.isActive, - 'deactivatedAt': aggregate.deactivatedAt?.toIso8601String(), + 'id': target.id.value, + 'name': target.name, + 'email': target.email, + 'isActive': target.isActive, + 'deactivatedAt': target.deactivatedAt?.toIso8601String(), }); } } @@ -149,14 +149,14 @@ void main() async { // Isar, SharedPreferences, SQLite, etc. final db = FakeLocalDatabase(); - // The adapter bridges the User aggregate to the local database. + // The adapter bridges the User target to the local database. final userAdapter = UserLocalDbAdapter(db: db); // Construct a StateBasedStore — identical setup as the backend // example, just with a different adapter implementation. final store = StateBasedStore( adapters: {User: userAdapter}, - aggregates: $aggregateList, + targets: $aggregateList, ); // TransactionalRunner manages session lifecycle. diff --git a/packages/continuum/example/lib/store_state_based_transactional.dart b/packages/continuum/example/lib/store_state_based_transactional.dart index e694835..fa66c73 100644 --- a/packages/continuum/example/lib/store_state_based_transactional.dart +++ b/packages/continuum/example/lib/store_state_based_transactional.dart @@ -2,15 +2,15 @@ /// /// Demonstrates how to use `StateBasedStore` with `TransactionalRunner` for /// apps backed by a traditional REST API. The backend is the source of truth — -/// it returns aggregates directly, and accepts commands like "update email" +/// it returns targets directly, and accepts commands like "update email" /// or "deactivate account". There is no event store; events exist only in /// memory as domain-level state mutations. /// /// What you'll learn: -/// - How to implement `AggregatePersistenceAdapter` backed by a REST API +/// - How to implement `TargetPersistenceAdapter` backed by a REST API /// - How `TransactionalRunner` manages session lifecycle (auto-commit) /// - How to run multiple mutations in a single transaction -/// - How the adapter receives the post-mutation aggregate and pending +/// - How the adapter receives the post-mutation target and pending /// operations, letting it choose how to translate them into API calls /// /// Real-world use case: A mobile or web app where the backend owns @@ -114,7 +114,7 @@ final class UserRecord { // ── Adapter ───────────────────────────────────────────────────────────────── -/// Bridges the `User` aggregate to the fake REST API. +/// Bridges the `User` target to the fake REST API. /// /// `fetchAsync` maps `GET /users/:id` → `User` domain object. /// `persistAsync` inspects pending operations and translates them @@ -122,7 +122,7 @@ final class UserRecord { /// mutations). /// /// In production, replace `FakeBackendApi` with a real HTTP client. -final class UserApiAdapter implements AggregatePersistenceAdapter { +final class UserApiAdapter implements TargetPersistenceAdapter { /// The backend API client injected at construction time. final FakeBackendApi _api; @@ -131,11 +131,11 @@ final class UserApiAdapter implements AggregatePersistenceAdapter { @override Future fetchAsync(StreamId streamId) async { - // Load user state from the backend and reconstruct the aggregate. + // Load user state from the backend and reconstruct the target. final record = await _api.getUserAsync(streamId.value); // The backend returns all fields — we reconstruct the domain - // aggregate using its creation factory. This is the only place + // target using its creation factory. This is the only place // where backend ↔ domain mapping happens. return User.createFromUserRegistered( UserRegistered( @@ -149,12 +149,12 @@ final class UserApiAdapter implements AggregatePersistenceAdapter { @override Future persistAsync( StreamId streamId, - User aggregate, + User target, List pendingOperations, ) async { // Inspect the pending operations to decide which backend calls // to make. The adapter has full flexibility here — it can send - // the final aggregate state, translate each operation into a + // the final target state, translate each operation into a // separate API call, or batch them. for (final operation in pendingOperations) { switch (operation) { @@ -162,15 +162,15 @@ final class UserApiAdapter implements AggregatePersistenceAdapter { // Creation event → POST new user to the backend. await _api.createUserAsync( streamId.value, - aggregate.name, - aggregate.email, + target.name, + target.email, ); case EmailChanged(): // Email mutation → PATCH with the new email. await _api.updateUserAsync( streamId.value, - email: aggregate.email, + email: target.email, ); case UserDeactivated(): @@ -178,7 +178,7 @@ final class UserApiAdapter implements AggregatePersistenceAdapter { await _api.updateUserAsync( streamId.value, isActive: false, - deactivatedAt: aggregate.deactivatedAt, + deactivatedAt: target.deactivatedAt, ); } } @@ -196,15 +196,15 @@ void main() async { // The backend API client — in production, inject your HTTP client here. final backendApi = FakeBackendApi(); - // The adapter bridges the User aggregate to the backend. + // The adapter bridges the User target to the backend. final userAdapter = UserApiAdapter(api: backendApi); - // Construct a StateBasedStore with one adapter per aggregate type. + // Construct a StateBasedStore with one adapter per target type. // $aggregateList provides the generated event-application registries - // so the session knows how to apply events to aggregates. + // so the session knows how to apply events to targets. final store = StateBasedStore( adapters: {User: userAdapter}, - aggregates: $aggregateList, + targets: $aggregateList, ); // TransactionalRunner manages session lifecycle automatically: @@ -224,7 +224,7 @@ void main() async { // Access the ambient session provided by the runner. final session = TransactionalRunner.currentSession; - // Apply the creation event — the session tracks the new aggregate + // Apply the creation event — the session tracks the new target // and the adapter will receive a UserRegistered operation on commit. final user = await session.applyAsync( userId, diff --git a/packages/continuum/example/lib/aggregate_creation.dart b/packages/continuum/example/lib/target_creation.dart similarity index 69% rename from packages/continuum/example/lib/aggregate_creation.dart rename to packages/continuum/example/lib/target_creation.dart index 1e8139b..6137fbd 100644 --- a/packages/continuum/example/lib/aggregate_creation.dart +++ b/packages/continuum/example/lib/target_creation.dart @@ -1,7 +1,7 @@ -/// Example 1: Creating Aggregates +/// Example 1: Creating Targets /// -/// This example shows how to create an aggregate from a creation event. -/// Every aggregate starts with a creation event that captures its initial state. +/// This example shows how to create a target from a creation event. +/// Every target starts with a creation event that captures its initial state. library; import 'package:continuum_example/domain/events/user_registered.dart'; @@ -9,12 +9,12 @@ import 'package:continuum_example/domain/user.dart'; void main() { print('═══════════════════════════════════════════════════════════════════'); - print('Example 1: Creating Aggregates from Events'); + print('Example 1: Creating Targets from Events'); print('═══════════════════════════════════════════════════════════════════'); print(''); - // Every aggregate begins its life with a creation event. - // The creation event captures all the data needed to initialize the aggregate. + // Every target begins its life with a creation event. + // The creation event captures all the data needed to initialize the target. final user = User.createFromUserRegistered( UserRegistered( userId: const UserId('user-123'), @@ -27,5 +27,5 @@ void main() { print(' $user'); print(''); - print('✓ The aggregate is now in memory and ready for mutations.'); + print('✓ The target is now in memory and ready for mutations.'); } diff --git a/packages/continuum/example/lib/aggregate_mutations.dart b/packages/continuum/example/lib/target_mutations.dart similarity index 96% rename from packages/continuum/example/lib/aggregate_mutations.dart rename to packages/continuum/example/lib/target_mutations.dart index 7b5995e..8b85f30 100644 --- a/packages/continuum/example/lib/aggregate_mutations.dart +++ b/packages/continuum/example/lib/target_mutations.dart @@ -1,6 +1,6 @@ /// Example 2: Applying Events to Change State /// -/// This example shows how to mutate aggregate state by applying events. +/// This example shows how to mutate target state by applying events. /// Each event represents a state transition with business meaning. library; diff --git a/packages/continuum/example/main.dart b/packages/continuum/example/main.dart index d1a10db..376c1b9 100644 --- a/packages/continuum/example/main.dart +++ b/packages/continuum/example/main.dart @@ -3,15 +3,15 @@ /// This package contains standalone examples demonstrating different aspects /// of event sourcing with Continuum. Each example is self-contained and runnable. /// -/// AGGREGATE FUNDAMENTALS: -/// aggregate_creation.dart - Creating aggregates from events -/// aggregate_mutations.dart - Mutating state by applying events +/// OPERATION TARGET FUNDAMENTALS: +/// target_creation.dart - Creating targets from events +/// target_mutations.dart - Mutating state by applying events /// event_replay.dart - Rebuilding state by replaying event history -/// abstract_interface_aggregates.dart - Abstract/interface aggregate support +/// abstract_interface_targets.dart - Abstract/interface target support /// /// PERSISTENCE (EventSourcingStore + Session): -/// store_creating_streams.dart - Creating new aggregate streams -/// store_loading_and_updating.dart - Loading and updating aggregates +/// store_creating_streams.dart - Creating new target streams +/// store_loading_and_updating.dart - Loading and updating targets /// store_handling_conflicts.dart - Detecting concurrency conflicts /// store_atomic_saves.dart - Atomic multi-stream saves /// store_atomic_rollback.dart - Atomic rollback on conflict @@ -39,14 +39,14 @@ void main() { print('Continuum Examples'); print('═══════════════════════════════════════════════════════════════════'); print(''); - print('AGGREGATE FUNDAMENTALS:'); - print(' aggregate_creation.dart - Creating aggregates from events'); - print(' aggregate_mutations.dart - Mutating state by applying events'); + print('OPERATION TARGET FUNDAMENTALS:'); + print(' target_creation.dart - Creating targets from events'); + print(' target_mutations.dart - Mutating state by applying events'); print(' event_replay.dart - Rebuilding state by replaying history'); - print(' abstract_interface_aggregates.dart - Abstract/interface support'); + print(' abstract_interface_targets.dart - Abstract/interface target support'); print(''); print('PERSISTENCE (EventSourcingStore + Session):'); - print(' store_creating_streams.dart - Creating aggregate streams'); + print(' store_creating_streams.dart - Creating target streams'); print(' store_loading_and_updating.dart - Loading and updating'); print(' store_handling_conflicts.dart - Conflict detection'); print(' store_atomic_saves.dart - Atomic multi-stream saves'); @@ -64,6 +64,6 @@ void main() { print(' (removed - see store_state_based_transactional.dart)'); print(''); print('Run any example:'); - print(' dart run aggregate_creation.dart'); + print(' dart run target_creation.dart'); print(''); } diff --git a/packages/continuum/lib/continuum.dart b/packages/continuum/lib/continuum.dart index 83a6988..eeadc23 100644 --- a/packages/continuum/lib/continuum.dart +++ b/packages/continuum/lib/continuum.dart @@ -1,4 +1,4 @@ -/// Continuum - Core types and annotations for event-sourced aggregates. +/// Continuum - Core types and annotations for operation-driven mutation. /// /// Provides annotations, event contracts, identity types, and dispatch /// registries used by generated code. Persistence and session types @@ -11,6 +11,8 @@ export 'package:zooper_flutter_core/zooper_flutter_core.dart' show EventId; // Annotations for code generation discovery export 'src/annotations/aggregate_event.dart'; +export 'src/annotations/operation_for.dart'; +export 'src/annotations/operation_target.dart'; export 'src/annotations/projection.dart'; // Continuum event base contract diff --git a/packages/continuum/lib/src/annotations/aggregate_event.dart b/packages/continuum/lib/src/annotations/aggregate_event.dart index 8134075..ac3012d 100644 --- a/packages/continuum/lib/src/annotations/aggregate_event.dart +++ b/packages/continuum/lib/src/annotations/aggregate_event.dart @@ -1,18 +1,19 @@ -/// Marks a class as a domain event belonging to a specific aggregate. +/// Legacy annotation associating a [ContinuumEvent] with a target type. /// -/// The generator uses this annotation to discover events and associate them -/// with their parent aggregate for code generation. +/// The generator still supports this annotation for backwards compatibility, +/// but new code should use [OperationFor]. /// /// The [type] parameter is optional when using the core layer without /// persistence. When persistence is needed, [type] provides a stable string /// discriminator for serialization. /// /// ```dart -/// @AggregateEvent(of: ShoppingCart) +/// @OperationFor(type: ShoppingCart, key: 'cart.item_added') /// class ItemAdded implements ContinuumEvent { /// // event implementation /// } /// ``` +@Deprecated('Use OperationFor(...) for operation-driven mutation instead. AggregateEvent will be removed in a future major release.') class AggregateEvent { /// The aggregate type this event belongs to. /// diff --git a/packages/continuum/lib/src/annotations/operation_for.dart b/packages/continuum/lib/src/annotations/operation_for.dart new file mode 100644 index 0000000..e6d9476 --- /dev/null +++ b/packages/continuum/lib/src/annotations/operation_for.dart @@ -0,0 +1,29 @@ +/// Marks a class as an operation that can be applied to a specific target type. +/// +/// The generator uses this annotation to associate operation types with their +/// targets and generate dispatch and registry wiring. +/// +/// The [key] parameter provides an optional stable discriminator for +/// persistence serialization. When null, the operation can still be used for +/// in-memory mutation but cannot be persisted without an explicit mapping. +/// +/// When [creation] is `true`, the generator treats this operation as a +/// creation operation and requires the target to declare a matching static +/// factory method `createFrom(Operation operation)`. +final class OperationFor { + /// The target type this operation belongs to. + final Type type; + + /// Optional stable key discriminator for persistence serialization. + final String? key; + + /// Whether this is a creation operation (the first operation in a stream). + final bool creation; + + /// Creates an operation annotation associating this operation with [type]. + const OperationFor({ + required this.type, + this.key, + this.creation = false, + }); +} diff --git a/packages/continuum/lib/src/annotations/operation_target.dart b/packages/continuum/lib/src/annotations/operation_target.dart new file mode 100644 index 0000000..2d74962 --- /dev/null +++ b/packages/continuum/lib/src/annotations/operation_target.dart @@ -0,0 +1,11 @@ +/// Marks a class as a target for operation-driven mutation. +/// +/// The code generator uses this annotation to discover types that should +/// receive generated operation dispatch helpers and registry entries. +/// +/// This annotation exists to avoid requiring a specific base class (such as +/// `AggregateRoot`) for discovery. +final class OperationTarget { + /// Creates an operation target marker. + const OperationTarget(); +} diff --git a/packages/continuum/lib/src/persistence/generated_aggregate.dart b/packages/continuum/lib/src/persistence/generated_aggregate.dart index 09eecc1..89b7ab8 100644 --- a/packages/continuum/lib/src/persistence/generated_aggregate.dart +++ b/packages/continuum/lib/src/persistence/generated_aggregate.dart @@ -18,7 +18,7 @@ import 'event_serializer_registry.dart'; /// // Usage: /// final store = EventSourcingStore( /// eventStore: InMemoryEventStore(), -/// aggregates: [$User, $Account], +/// targets: [$User, $Account], /// ); /// ``` final class GeneratedAggregate { diff --git a/packages/continuum/test/annotations/operation_for_test.dart b/packages/continuum/test/annotations/operation_for_test.dart new file mode 100644 index 0000000..723a38d --- /dev/null +++ b/packages/continuum/test/annotations/operation_for_test.dart @@ -0,0 +1,36 @@ +import 'package:continuum/continuum.dart'; +import 'package:test/test.dart'; + +final class _TargetType { + const _TargetType(); +} + +void main() { + group('OperationFor', () { + test('should store type, key, and creation flag', () { + // Arrange + const key = 'my.operation.key'; + + // Act + const annotation = OperationFor( + type: _TargetType, + key: key, + creation: true, + ); + + // Assert + expect(annotation.type, equals(_TargetType)); + expect(annotation.key, equals(key)); + expect(annotation.creation, isTrue); + }); + + test('should default key to null and creation to false', () { + // Arrange & Act + const annotation = OperationFor(type: _TargetType); + + // Assert + expect(annotation.key, isNull); + expect(annotation.creation, isFalse); + }); + }); +} diff --git a/packages/continuum/test/annotations/operation_target_test.dart b/packages/continuum/test/annotations/operation_target_test.dart new file mode 100644 index 0000000..3fbc6f2 --- /dev/null +++ b/packages/continuum/test/annotations/operation_target_test.dart @@ -0,0 +1,14 @@ +import 'package:continuum/continuum.dart'; +import 'package:test/test.dart'; + +void main() { + group('OperationTarget', () { + test('should be const-constructible', () { + // Arrange & Act + const annotation = OperationTarget(); + + // Assert + expect(annotation, isA()); + }); + }); +} diff --git a/packages/continuum_event_sourcing/README.md b/packages/continuum_event_sourcing/README.md index 7a4140a..d85835f 100644 --- a/packages/continuum_event_sourcing/README.md +++ b/packages/continuum_event_sourcing/README.md @@ -28,7 +28,7 @@ import 'continuum.g.dart'; final store = EventSourcingStore( eventStore: InMemoryEventStore(), - aggregates: $aggregateList, + targets: $aggregateList, ); final session = store.openSession(); @@ -42,7 +42,7 @@ await session.saveChangesAsync(); ### Serialization -Events are serialized to JSON via `JsonEventSerializer`. Implement `toJson()` / `fromJson()` on your events and register them through `@AggregateEvent(type: '...')`. +Events are serialized to JSON via `JsonEventSerializer`. Implement `toJson()` / `fromJson()` on your events and register them through `@OperationFor(type: YourTarget, key: '...')`. ### Projections diff --git a/packages/continuum_event_sourcing/lib/src/persistence/event_sourcing_store.dart b/packages/continuum_event_sourcing/lib/src/persistence/event_sourcing_store.dart index 47c5c9a..e6abb80 100644 --- a/packages/continuum_event_sourcing/lib/src/persistence/event_sourcing_store.dart +++ b/packages/continuum_event_sourcing/lib/src/persistence/event_sourcing_store.dart @@ -30,32 +30,45 @@ final class EventSourcingStore implements SessionStore { /// Controls when events mutate the in-memory aggregate. final EventApplicationMode _applicationMode; - /// Creates an event sourcing store from generated aggregate bundles. + /// Creates an event sourcing store from generated target bundles. /// /// This is the recommended constructor. Pass all your generated - /// aggregate bundles (e.g., `$User`, `$Account`) and the store + /// target bundles (e.g., `$User`, `$Account`) and the store /// will automatically merge their registries. /// /// Optionally provide an [applicationMode] to control when events /// mutate the in-memory aggregate. Defaults to [EventApplicationMode.eager]. factory EventSourcingStore({ required EventStore eventStore, - required List aggregates, + List? targets, + @Deprecated('Use targets instead. This alias will be removed in a future major release.') List? aggregates, EventApplicationMode applicationMode = EventApplicationMode.eager, }) { - // Merge all registries from the provided aggregates + final List? resolvedTargets = targets ?? aggregates; + if (resolvedTargets == null) { + throw ArgumentError( + 'EventSourcingStore requires either `targets` or `aggregates`.', + ); + } + if (targets != null && aggregates != null) { + throw ArgumentError( + 'Provide only one of `targets` or `aggregates`.', + ); + } + + // Merge all registries from the provided targets. var serializerRegistry = const EventSerializerRegistry.empty(); var aggregateFactories = const AggregateFactoryRegistry.empty(); var eventAppliers = const EventApplierRegistry.empty(); - for (final aggregate in aggregates) { + for (final target in resolvedTargets) { serializerRegistry = serializerRegistry.merge( - aggregate.serializerRegistry, + target.serializerRegistry, ); aggregateFactories = aggregateFactories.merge( - aggregate.aggregateFactories, + target.aggregateFactories, ); - eventAppliers = eventAppliers.merge(aggregate.eventAppliers); + eventAppliers = eventAppliers.merge(target.eventAppliers); } return EventSourcingStore._( diff --git a/packages/continuum_event_sourcing/test/persistence/event_application_mode_test.dart b/packages/continuum_event_sourcing/test/persistence/event_application_mode_test.dart index e810124..53c5258 100644 --- a/packages/continuum_event_sourcing/test/persistence/event_application_mode_test.dart +++ b/packages/continuum_event_sourcing/test/persistence/event_application_mode_test.dart @@ -46,7 +46,7 @@ void main() { setUp(() { store = EventSourcingStore( eventStore: eventStore, - aggregates: [buildGeneratedCounterAggregate()], + targets: [buildGeneratedCounterAggregate()], applicationMode: EventApplicationMode.eager, ); }); @@ -130,7 +130,7 @@ void main() { setUp(() { store = EventSourcingStore( eventStore: eventStore, - aggregates: [buildGeneratedCounterAggregate()], + targets: [buildGeneratedCounterAggregate()], applicationMode: EventApplicationMode.deferred, ); }); diff --git a/packages/continuum_event_sourcing/test/persistence/session_apply_async_test.dart b/packages/continuum_event_sourcing/test/persistence/session_apply_async_test.dart index 9ba5f2a..27f64a3 100644 --- a/packages/continuum_event_sourcing/test/persistence/session_apply_async_test.dart +++ b/packages/continuum_event_sourcing/test/persistence/session_apply_async_test.dart @@ -19,7 +19,7 @@ void main() { eventStore = MockEventStore(); store = EventSourcingStore( eventStore: eventStore, - aggregates: [buildGeneratedCounterAggregate()], + targets: [buildGeneratedCounterAggregate()], ); serializer = JsonEventSerializer( registry: buildCounterSerializerRegistry(), diff --git a/packages/continuum_event_sourcing/test/persistence/session_concurrency_retry_test.dart b/packages/continuum_event_sourcing/test/persistence/session_concurrency_retry_test.dart index 9a7923b..ddd1614 100644 --- a/packages/continuum_event_sourcing/test/persistence/session_concurrency_retry_test.dart +++ b/packages/continuum_event_sourcing/test/persistence/session_concurrency_retry_test.dart @@ -22,7 +22,7 @@ void main() { eventStore = MockEventStore(); store = EventSourcingStore( eventStore: eventStore, - aggregates: [buildGeneratedCounterAggregate()], + targets: [buildGeneratedCounterAggregate()], ); serializer = JsonEventSerializer( registry: buildCounterSerializerRegistry(), @@ -537,7 +537,7 @@ void main() { final atomicStore = MockAtomicEventStore(); final store = EventSourcingStore( eventStore: atomicStore, - aggregates: [buildGeneratedCounterAggregate()], + targets: [buildGeneratedCounterAggregate()], ); final streamA = const StreamId('counter-a'); diff --git a/packages/continuum_event_sourcing/test/persistence/session_load_all_async_test.dart b/packages/continuum_event_sourcing/test/persistence/session_load_all_async_test.dart index db93d98..9595467 100644 --- a/packages/continuum_event_sourcing/test/persistence/session_load_all_async_test.dart +++ b/packages/continuum_event_sourcing/test/persistence/session_load_all_async_test.dart @@ -21,7 +21,7 @@ void main() { eventStore = MockEventStore(); store = EventSourcingStore( eventStore: eventStore, - aggregates: [buildGeneratedCounterAggregate()], + targets: [buildGeneratedCounterAggregate()], ); serializer = JsonEventSerializer( registry: buildCounterSerializerRegistry(), @@ -286,7 +286,7 @@ void main() { final atomicStore = MockAtomicEventStore(); final eventSourcingStore = EventSourcingStore( eventStore: atomicStore, - aggregates: [buildGeneratedCounterAggregate()], + targets: [buildGeneratedCounterAggregate()], ); when(atomicStore.appendEventsToStreamsAsync(any)).thenAnswer((_) async {}); diff --git a/packages/continuum_event_sourcing/test/persistence/session_test.dart b/packages/continuum_event_sourcing/test/persistence/session_test.dart index 6eaa23e..4edc2ef 100644 --- a/packages/continuum_event_sourcing/test/persistence/session_test.dart +++ b/packages/continuum_event_sourcing/test/persistence/session_test.dart @@ -17,7 +17,7 @@ void main() { test('loadAsync caches within a session (one store load)', () async { final eventStore = MockEventStore(); final aggregate = buildGeneratedCounterAggregate(); - final store = EventSourcingStore(eventStore: eventStore, aggregates: [aggregate]); + final store = EventSourcingStore(eventStore: eventStore, targets: [aggregate]); final serializer = JsonEventSerializer(registry: buildCounterSerializerRegistry()); final streamId = const StreamId('counter-1'); @@ -64,7 +64,7 @@ void main() { test('discardStream removes pending events but keeps mutated state', () async { final eventStore = MockEventStore(); final aggregate = buildGeneratedCounterAggregate(); - final store = EventSourcingStore(eventStore: eventStore, aggregates: [aggregate]); + final store = EventSourcingStore(eventStore: eventStore, targets: [aggregate]); final session = store.openSession(); final streamId = const StreamId('counter-2'); @@ -94,7 +94,7 @@ void main() { () async { final eventStore = MockEventStore(); final aggregate = buildGeneratedCounterAggregate(); - final store = EventSourcingStore(eventStore: eventStore, aggregates: [aggregate]); + final store = EventSourcingStore(eventStore: eventStore, targets: [aggregate]); final session = store.openSession(); when(eventStore.appendEventsAsync(any, any, any, aggregateType: anyNamed('aggregateType'))).thenAnswer((_) async {}); @@ -131,7 +131,7 @@ void main() { test('saveChangesAsync uses ExpectedVersion.exact for loaded streams', () async { final eventStore = MockEventStore(); final aggregate = buildGeneratedCounterAggregate(); - final store = EventSourcingStore(eventStore: eventStore, aggregates: [aggregate]); + final store = EventSourcingStore(eventStore: eventStore, targets: [aggregate]); final serializer = JsonEventSerializer(registry: buildCounterSerializerRegistry()); final streamId = const StreamId('counter-existing'); @@ -186,7 +186,7 @@ void main() { }); final aggregate = buildGeneratedCounterAggregate(); - final store = EventSourcingStore(eventStore: eventStore, aggregates: [aggregate]); + final store = EventSourcingStore(eventStore: eventStore, targets: [aggregate]); final session = store.openSession(); final s1 = const StreamId('counter-a'); diff --git a/packages/continuum_generator/README.md b/packages/continuum_generator/README.md index 1af9383..9c20183 100644 --- a/packages/continuum_generator/README.md +++ b/packages/continuum_generator/README.md @@ -1,14 +1,14 @@ # Continuum Generator -Code generator for the [continuum](../continuum) event sourcing library. Automatically generates event handling code and discovers all aggregates in your project. +Code generator for the [continuum](../continuum) event sourcing library. Automatically generates event handling code and discovers all operation targets in your project. ## What It Generates -### Per-Aggregate Files (`*.g.dart`) +### Per-Target Files (`*.g.dart`) -For each `AggregateRoot()` class, generates: +For each operation target (a class annotated with `@OperationTarget()`, or a legacy `AggregateRoot` class), generates: -1. **Event handler mixin** (`_$YourAggregateEventHandlers`) +1. **Event handler mixin** (`_$YourTypeEventHandlers`) - Connects your `applyEventName()` methods to events - Type-safe event dispatching @@ -24,7 +24,7 @@ For each `AggregateRoot()` class, generates: ### Global Discovery (`lib/continuum.g.dart`) -Automatically discovers all `AggregateRoot()` classes and generates: +Automatically discovers all operation targets and generates: ```dart final List $aggregateList = [ @@ -39,7 +39,7 @@ This enables zero-configuration setup: ```dart final store = EventSourcingStore( eventStore: myStore, - aggregates: $aggregateList, // Just works! + targets: $aggregateList, // Just works! ); ``` @@ -65,10 +65,13 @@ import 'package:continuum/continuum.dart'; part 'user.g.dart'; -class User extends AggregateRoot with _$UserEventHandlers { +@OperationTarget() +class User with _$UserEventHandlers { + User._({required this.id, required this.email}); + + final String id; String email; - User._({required super.id, required this.email}); static User createFromUserCreated(UserCreated event) { return User._(id: event.userId, email: event.email); } @@ -79,7 +82,7 @@ class User extends AggregateRoot with _$UserEventHandlers { } } -@AggregateEvent(of: User, type: 'user.created') +@OperationFor(type: User, key: 'user.created', creation: true) class UserCreated implements ContinuumEvent { UserCreated({ required this.userId, @@ -122,7 +125,7 @@ class UserCreated implements ContinuumEvent { } } -@AggregateEvent(of: User, type: 'user.email_changed') +@OperationFor(type: User, key: 'user.email_changed') class EmailChanged implements ContinuumEvent { EmailChanged({ required this.userId, @@ -192,7 +195,7 @@ void main() { // Zero-configuration setup! final store = EventSourcingStore( eventStore: InMemoryEventStore(), - aggregates: $aggregateList, + targets: $aggregateList, ); final userId = StreamId('123'); @@ -280,7 +283,7 @@ Each package gets its own `continuum.g.dart` with its aggregates. ## How Auto-Discovery Works -The generator scans all `.dart` files in your `lib/` directory for `AggregateRoot` classes and collects them into `$aggregateList`. This happens in a separate build phase after all per-aggregate generators complete. +The generator scans all `.dart` files in your `lib/` directory for operation targets and collects them into `$aggregateList`. This happens in a separate build phase after all per-target generators complete. **You don't need to:** - Manually import aggregate files @@ -288,7 +291,7 @@ The generator scans all `.dart` files in your `lib/` directory for `AggregateRoo - Merge multiple registries **Just:** -1. Extend `AggregateRoot` in your aggregate class +1. Annotate your target class with `@OperationTarget()` (or extend `AggregateRoot` as a legacy marker) 2. Run `build_runner` 3. Use `$aggregateList` @@ -305,7 +308,7 @@ dart run build_runner build Make sure: 1. You have `part 'my_aggregate.g.dart';` directive -2. Your class extends `AggregateRoot` and mixes in `_$MyAggregateEventHandlers`: +2. Your class is an operation target and mixes in `_$MyAggregateEventHandlers`: 3. You've run `build_runner` ### "No apply method found for event" diff --git a/packages/continuum_generator/lib/src/aggregate_discovery.dart b/packages/continuum_generator/lib/src/aggregate_discovery.dart index b652623..bd2db95 100644 --- a/packages/continuum_generator/lib/src/aggregate_discovery.dart +++ b/packages/continuum_generator/lib/src/aggregate_discovery.dart @@ -8,8 +8,14 @@ import 'models/event_info.dart'; /// Type checker for bounded's AggregateRoot base class. const _aggregateRootChecker = TypeChecker.fromUrl('package:bounded/src/aggregate_root.dart#AggregateRoot'); -/// Type checker for the @AggregateEvent annotation. -const _eventChecker = TypeChecker.fromUrl('package:continuum/src/annotations/aggregate_event.dart#AggregateEvent'); +/// Type checker for the @OperationTarget annotation. +const _operationTargetChecker = TypeChecker.fromUrl('package:continuum/src/annotations/operation_target.dart#OperationTarget'); + +/// Type checker for the legacy @AggregateEvent annotation. +const _aggregateEventChecker = TypeChecker.fromUrl('package:continuum/src/annotations/aggregate_event.dart#AggregateEvent'); + +/// Type checker for the @OperationFor annotation. +const _operationForChecker = TypeChecker.fromUrl('package:continuum/src/annotations/operation_for.dart#OperationFor'); /// Type checker for the ContinuumEvent base class. const _continuumEventChecker = TypeChecker.fromUrl('package:continuum/src/events/continuum_event.dart#ContinuumEvent'); @@ -48,9 +54,11 @@ final class AggregateDiscovery { // First pass: discover all aggregates in THIS library. // - // Aggregates are discovered by being assignable to bounded's AggregateRoot. + // Targets are discovered by either: + // - being annotated with [@OperationTarget] (preferred) + // - being assignable to bounded's [AggregateRoot] (legacy; deprecated marker) for (final element in library.classes) { - if (_aggregateRootChecker.isAssignableFrom(element)) { + if (_isOperationTarget(element)) { final aggregateName = element.name ?? element.displayName; if (aggregateName.isEmpty) continue; aggregates[aggregateName] = AggregateInfo(element: element); @@ -81,7 +89,7 @@ final class AggregateDiscovery { for (final candidateLibrary in librariesToScan) { for (final element in candidateLibrary.classes) { - if (!_eventChecker.hasAnnotationOf(element)) continue; + if (!_hasOperationAnnotation(element)) continue; final eventInfo = _extractEventInfo(element); if (eventInfo == null) continue; @@ -121,24 +129,30 @@ final class AggregateDiscovery { return null; } - final annotation = _eventChecker.firstAnnotationOf(element); + final annotation = _operationForChecker.firstAnnotationOf(element) ?? _aggregateEventChecker.firstAnnotationOf(element); if (annotation == null) return null; - // Extract the of type - final ofAggregateValue = annotation.getField('of'); - if (ofAggregateValue == null || ofAggregateValue.isNull) return null; + final bool isOperationFor = _operationForChecker.hasAnnotationOf(element); + + // Extract the associated target type. + // - OperationFor: `type` + // - AggregateEvent: `of` + final targetTypeValue = isOperationFor ? annotation.getField('type') : annotation.getField('of'); + if (targetTypeValue == null || targetTypeValue.isNull) return null; - final aggregateType = ofAggregateValue.toTypeValue(); + final aggregateType = targetTypeValue.toTypeValue(); if (aggregateType == null) return null; final aggregateTypeName = _getTypeName(aggregateType); if (aggregateTypeName == null) return null; - // Extract the optional type discriminator - final typeValue = annotation.getField('type'); - final type = typeValue?.toStringValue(); + // Extract the optional stable discriminator. + // - OperationFor: `key` + // - AggregateEvent: `type` + final discriminatorValue = isOperationFor ? annotation.getField('key') : annotation.getField('type'); + final type = discriminatorValue?.toStringValue(); - // Determine if this is a creation event via explicit annotation flag. + // Determine if this is a creation operation/event via explicit annotation flag. final creationValue = annotation.getField('creation'); final bool isCreationEvent = creationValue?.toBoolValue() ?? false; @@ -152,6 +166,19 @@ final class AggregateDiscovery { return EventInfo(element: element, aggregateTypeName: aggregateTypeName, type: type, isCreationEvent: isCreationEvent); } + bool _isOperationTarget(ClassElement classElement) { + if (_operationTargetChecker.hasAnnotationOf(classElement)) { + return true; + } + + // Legacy marker for discovery. + return _aggregateRootChecker.isAssignableFrom(classElement); + } + + bool _hasOperationAnnotation(ClassElement classElement) { + return _operationForChecker.hasAnnotationOf(classElement) || _aggregateEventChecker.hasAnnotationOf(classElement); + } + /// Gets the type name from a DartType. String? _getTypeName(DartType type) { final element = type.element; diff --git a/packages/continuum_generator/lib/src/combining_builder.dart b/packages/continuum_generator/lib/src/combining_builder.dart index ad8dc46..74d495e 100644 --- a/packages/continuum_generator/lib/src/combining_builder.dart +++ b/packages/continuum_generator/lib/src/combining_builder.dart @@ -16,13 +16,16 @@ const List _generatedDartFileSuffixesToIgnore = [ /// A builder that combines all discovered aggregates and projections into a single file. /// /// This builder runs after all per-aggregate and per-projection generators have completed. -/// It scans the entire package for bounded `AggregateRoot` types and `@Projection()` annotations and generates -/// a single `lib/continuum.g.dart` file containing `$aggregateList`, `$projectionList`, +/// It scans the entire package for discoverable operation targets and `@Projection()` annotations and generates +/// a single `lib/continuum.g.dart` file containing `$targetList` (and the deprecated `$aggregateList` alias), `$projectionList`, /// and a `registerAll` extension method on [ProjectionRegistry]. class CombiningBuilder implements Builder { /// Type checker for bounded's AggregateRoot base class. static const _aggregateRootChecker = TypeChecker.fromUrl('package:bounded/src/aggregate_root.dart#AggregateRoot'); + /// Type checker for the @OperationTarget annotation. + static const _operationTargetChecker = TypeChecker.fromUrl('package:continuum/src/annotations/operation_target.dart#OperationTarget'); + /// Discovery for extracting full projection metadata. final _projectionDiscovery = ProjectionDiscovery(); @@ -54,7 +57,7 @@ class CombiningBuilder implements Builder { // Find all classes assignable to AggregateRoot. for (final element in library.classes) { - if (_aggregateRootChecker.isAssignableFrom(element)) { + if (_aggregateRootChecker.isAssignableFrom(element) || _operationTargetChecker.hasAnnotationOf(element)) { aggregateInfos.add( _DiscoveredInfo( className: element.displayName, @@ -133,13 +136,13 @@ class CombiningBuilder implements Builder { buffer.writeln(); - // Generate $aggregateList if any aggregates found. + // Generate $targetList if any targets found. if (aggregateInfos.isNotEmpty) { - buffer.writeln('/// All discovered aggregates in this package.'); + buffer.writeln('/// All discovered operation targets in this package.'); buffer.writeln('///'); buffer.writeln('/// Pass this list to [EventSourcingStore] for automatic'); buffer.writeln('/// registration of all serializers, factories, and appliers.'); - buffer.writeln('final List \$aggregateList = ['); + buffer.writeln('final List \$targetList = ['); for (final info in aggregateInfos) { buffer.writeln(' \$${info.className},'); @@ -147,6 +150,13 @@ class CombiningBuilder implements Builder { buffer.writeln('];'); buffer.writeln(); + + buffer.writeln('/// Backward-compatible alias for [\$targetList].'); + buffer.writeln('///'); + buffer.writeln('/// This package historically exposed discovered targets as `\$aggregateList`.'); + buffer.writeln("@Deprecated('Use \$targetList instead.')"); + buffer.writeln('final List \$aggregateList = \$targetList;'); + buffer.writeln(); } // Generate $projectionList if any projections found. diff --git a/packages/continuum_generator/test/aggregate_discovery_test.dart b/packages/continuum_generator/test/aggregate_discovery_test.dart index 836fba3..de378aa 100644 --- a/packages/continuum_generator/test/aggregate_discovery_test.dart +++ b/packages/continuum_generator/test/aggregate_discovery_test.dart @@ -7,6 +7,98 @@ import 'package:test/test.dart'; void main() { group('AggregateDiscovery', () { + test('discovers @OperationTarget types without AggregateRoot', () async { + // Arrange + final inputs = { + 'continuum_generator|lib/domain.dart': r""" +import 'package:continuum/continuum.dart'; + +@OperationTarget() +class UserProfile { + UserProfile(); +} +""", + }; + + // Act + final aggregates = await resolveSources( + inputs, + (resolver) async { + final library = await _libraryFor(resolver, 'continuum_generator|lib/domain.dart'); + return AggregateDiscovery().discoverAggregates( + library, + candidateEventLibraries: [library], + ); + }, + rootPackage: 'continuum_generator', + readAllSourcesFromFilesystem: true, + ); + + // Assert + expect(aggregates, hasLength(1)); + expect(aggregates.single.name, 'UserProfile'); + }); + + test('associates @OperationFor operations to @OperationTarget types', () async { + // Arrange + final inputs = { + 'continuum_generator|lib/domain.dart': r""" +import 'package:continuum/continuum.dart'; + +@OperationTarget() +class UserProfile { + UserProfile(); +} + +@OperationFor(type: UserProfile, key: 'user_profile.renamed') +class UserProfileRenamed implements ContinuumEvent { + UserProfileRenamed({ + EventId? eventId, + DateTime? occurredOn, + Map metadata = const {}, + }) : id = eventId ?? EventId.fromUlid(), + occurredOn = occurredOn ?? DateTime(2020, 1, 1), + metadata = Map.unmodifiable(metadata); + + @override + final EventId id; + + @override + final DateTime occurredOn; + + @override + final Map metadata; +} +""", + }; + + // Act + final aggregates = await resolveSources( + inputs, + (resolver) async { + final library = await _libraryFor(resolver, 'continuum_generator|lib/domain.dart'); + return AggregateDiscovery().discoverAggregates( + library, + candidateEventLibraries: [library], + ); + }, + rootPackage: 'continuum_generator', + readAllSourcesFromFilesystem: true, + ); + + // Assert + expect(aggregates, hasLength(1)); + expect(aggregates.single.name, 'UserProfile'); + expect( + aggregates.single.mutationEvents.map((e) => e.name), + contains('UserProfileRenamed'), + ); + expect( + aggregates.single.mutationEvents.single.type, + equals('user_profile.renamed'), + ); + }); + test('discovers abstract aggregate roots', () async { // Arrange final inputs = { diff --git a/packages/continuum_generator/test/combining_builder_test.dart b/packages/continuum_generator/test/combining_builder_test.dart index 48f38cf..82d3558 100644 --- a/packages/continuum_generator/test/combining_builder_test.dart +++ b/packages/continuum_generator/test/combining_builder_test.dart @@ -65,8 +65,10 @@ class User extends AggregateRoot { allOf( contains("import 'package:continuum/continuum.dart';"), contains("import 'user.dart';"), - contains(r'final List $aggregateList = ['), + contains(r'final List $targetList = ['), contains(r' $User,'), + contains(r"@Deprecated('Use $targetList instead.')"), + contains(r'final List $aggregateList = $targetList;'), ), ), }, diff --git a/packages/continuum_lints/example/lib/bad_audio_file.dart b/packages/continuum_lints/example/lib/bad_audio_file.dart index 912089f..0434182 100644 --- a/packages/continuum_lints/example/lib/bad_audio_file.dart +++ b/packages/continuum_lints/example/lib/bad_audio_file.dart @@ -1,3 +1,5 @@ +// ignore_for_file: continuum_missing_apply_handlers, continuum_missing_creation_factories + import 'package:bounded/bounded.dart'; import 'package:continuum/continuum.dart'; @@ -11,7 +13,7 @@ final class AudioFileId extends TypedIdentity { /// /// This event is explicitly marked as a creation event, which means the /// aggregate must define a matching `createFromAudioFileCreated(...)` factory. -@AggregateEvent(of: AudioFile, creation: true) +@OperationFor(type: AudioFile, creation: true) abstract class AudioFileCreated implements ContinuumEvent { /// Creates a test event instance. const AudioFileCreated(); @@ -44,10 +46,12 @@ mixin _$AudioFileEventHandlers { /// - `continuum_missing_creation_factories`: the [AudioFileCreated] event is /// marked as a creation event but the aggregate does not define /// `createFromAudioFileCreated(...)`. -// ignore: continuum_missing_apply_handlers, continuum_missing_creation_factories -class AudioFile extends AggregateRoot with _$AudioFileEventHandlers { +@OperationTarget() +class AudioFile with _$AudioFileEventHandlers { /// Creates an [AudioFile]. - AudioFile(super.id); + AudioFile(this.id); + + final String id; /// Implements `noSuchMethod` so the class can remain concrete even though it /// does not implement all interface members. diff --git a/packages/continuum_lints/lib/src/continuum_implement_missing_apply_handlers_fix.dart b/packages/continuum_lints/lib/src/continuum_implement_missing_apply_handlers_fix.dart index 0926e4e..1ba7654 100644 --- a/packages/continuum_lints/lib/src/continuum_implement_missing_apply_handlers_fix.dart +++ b/packages/continuum_lints/lib/src/continuum_implement_missing_apply_handlers_fix.dart @@ -7,7 +7,7 @@ import 'package:custom_lint_builder/custom_lint_builder.dart'; import 'continuum_required_apply_handlers.dart'; /// Quick-fix that inserts stub implementations for missing `apply(...)` -/// handlers required by the generated `_$EventHandlers` mixin. +/// handlers required by the generated `_$EventHandlers` mixin. final class ContinuumImplementMissingApplyHandlersFix extends DartFix { static final Object _resolvedUnitKey = Object(); diff --git a/packages/continuum_lints/lib/src/continuum_missing_apply_handlers_rule.dart b/packages/continuum_lints/lib/src/continuum_missing_apply_handlers_rule.dart index ec5e108..9cc94b2 100644 --- a/packages/continuum_lints/lib/src/continuum_missing_apply_handlers_rule.dart +++ b/packages/continuum_lints/lib/src/continuum_missing_apply_handlers_rule.dart @@ -7,9 +7,9 @@ import 'package:custom_lint_builder/custom_lint_builder.dart'; import 'continuum_implement_missing_apply_handlers_fix.dart'; import 'continuum_required_apply_handlers.dart'; -/// Reports when a non-abstract aggregate root is missing required +/// Reports when a non-abstract operation target is missing required /// `apply(...)` handlers declared by the generated -/// `_$EventHandlers` mixin. +/// `_$EventHandlers` mixin. /// /// Why this exists: /// - Dart allows classes to become *implicitly abstract* when they do not @@ -20,12 +20,15 @@ import 'continuum_required_apply_handlers.dart'; final class ContinuumMissingApplyHandlersRule extends DartLintRule { static const LintCode _lintCode = LintCode( name: 'continuum_missing_apply_handlers', - problemMessage: 'This aggregate root mixes in generated event handlers but is missing apply methods: {0}.', + problemMessage: 'This operation target mixes in generated event handlers but is missing apply methods: {0}.', correctionMessage: 'Implement the missing apply(...) methods.', errorSeverity: DiagnosticSeverity.WARNING, ); static final TypeChecker _aggregateRootChecker = const TypeChecker.fromUrl('package:bounded/src/aggregate_root.dart#AggregateRoot'); + static final TypeChecker _operationTargetChecker = const TypeChecker.fromUrl( + 'package:continuum/src/annotations/operation_target.dart#OperationTarget', + ); const ContinuumMissingApplyHandlersRule() : super(code: _lintCode); @@ -44,7 +47,8 @@ final class ContinuumMissingApplyHandlersRule extends DartLintRule { final ClassElement? classElement = node.declaredFragment?.element; if (classElement == null) return; - if (!_aggregateRootChecker.isAssignableFrom(classElement)) return; + final bool isTarget = _aggregateRootChecker.isAssignableFrom(classElement) || _operationTargetChecker.hasAnnotationOf(classElement); + if (!isTarget) return; // If the user explicitly made the class abstract, they can defer handler // implementations to concrete subtypes. diff --git a/packages/continuum_lints/lib/src/continuum_missing_creation_factories_rule.dart b/packages/continuum_lints/lib/src/continuum_missing_creation_factories_rule.dart index 6993aad..35aa169 100644 --- a/packages/continuum_lints/lib/src/continuum_missing_creation_factories_rule.dart +++ b/packages/continuum_lints/lib/src/continuum_missing_creation_factories_rule.dart @@ -7,24 +7,28 @@ import 'package:custom_lint_builder/custom_lint_builder.dart'; import 'continuum_implement_missing_creation_factories_fix.dart'; import 'continuum_required_creation_factories.dart'; -/// Reports when an aggregate root is missing one or more required -/// `createFrom(Event event)` creation factory methods for its creation -/// events. +/// Reports when an operation target is missing one or more required +/// `createFrom(Operation operation)` creation factory methods for +/// its creation operations. /// /// Why this exists: -/// - Creation event classification is explicit via `@AggregateEvent(creation: true)`. -/// - When a creation event exists but the factory method is missing, the +/// - Creation classification is explicit via `@OperationFor(creation: true)`. +/// - Legacy projects may still use `@AggregateEvent(creation: true)`. +/// - When a creation operation exists but the factory method is missing, the /// generator will fail, but this lint surfaces the issue immediately in the /// editor. final class ContinuumMissingCreationFactoriesRule extends DartLintRule { static const LintCode _lintCode = LintCode( name: 'continuum_missing_creation_factories', - problemMessage: 'This aggregate root is missing required creation factories: {0}.', + problemMessage: 'This operation target is missing required creation factories: {0}.', correctionMessage: 'Add the missing static createFrom(Event event) factory methods.', errorSeverity: DiagnosticSeverity.WARNING, ); static final TypeChecker _aggregateRootChecker = const TypeChecker.fromUrl('package:bounded/src/aggregate_root.dart#AggregateRoot'); + static final TypeChecker _operationTargetChecker = const TypeChecker.fromUrl( + 'package:continuum/src/annotations/operation_target.dart#OperationTarget', + ); const ContinuumMissingCreationFactoriesRule() : super(code: _lintCode); @@ -43,7 +47,8 @@ final class ContinuumMissingCreationFactoriesRule extends DartLintRule { final ClassElement? classElement = node.declaredFragment?.element; if (classElement == null) return; - if (!_aggregateRootChecker.isAssignableFrom(classElement)) return; + final bool isTarget = _aggregateRootChecker.isAssignableFrom(classElement) || _operationTargetChecker.hasAnnotationOf(classElement); + if (!isTarget) return; final List missingFactories = const ContinuumRequiredCreationFactories().findMissingCreationFactories(classElement); if (missingFactories.isEmpty) return; diff --git a/packages/continuum_lints/lib/src/continuum_required_apply_handlers.dart b/packages/continuum_lints/lib/src/continuum_required_apply_handlers.dart index 61a707c..782c757 100644 --- a/packages/continuum_lints/lib/src/continuum_required_apply_handlers.dart +++ b/packages/continuum_lints/lib/src/continuum_required_apply_handlers.dart @@ -2,21 +2,25 @@ import 'package:analyzer/dart/element/element.dart'; import 'package:analyzer/dart/element/type.dart'; import 'package:custom_lint_builder/custom_lint_builder.dart'; -/// Computes which `apply...` handlers a concrete aggregate is still missing. +/// Computes which `apply...` handlers a concrete operation target is missing. /// /// The handler requirements are derived from the generated -/// `_$EventHandlers` mixin. +/// `_$EventHandlers` mixin. final class ContinuumRequiredApplyHandlers { static final TypeChecker _aggregateEventChecker = const TypeChecker.fromUrl( 'package:continuum/src/annotations/aggregate_event.dart#AggregateEvent', ); + static final TypeChecker _operationForChecker = const TypeChecker.fromUrl( + 'package:continuum/src/annotations/operation_for.dart#OperationFor', + ); + /// Creates a requirement checker for generated continuum apply handlers. const ContinuumRequiredApplyHandlers(); /// Returns the list of missing apply handler method names. /// - /// If the class does not mix in `_$EventHandlers`, this returns an + /// If the class does not mix in `_$EventHandlers`, this returns an /// empty list. List findMissingApplyHandlers(ClassElement classElement) { final List missingMethods = findMissingApplyHandlerMethods(classElement); @@ -28,7 +32,7 @@ final class ContinuumRequiredApplyHandlers { /// /// This is useful for generating method stubs in quick-fixes. /// - /// If the class does not mix in `_$EventHandlers`, this returns an + /// If the class does not mix in `_$EventHandlers`, this returns an /// empty list. List findMissingApplyHandlerMethods(ClassElement classElement) { final InterfaceType? eventHandlersMixinType = _findEventHandlersMixinType(classElement); @@ -68,12 +72,15 @@ final class ContinuumRequiredApplyHandlers { final Element? eventElement = parameter.type.element; if (eventElement is! ClassElement) return false; - if (!_aggregateEventChecker.hasAnnotationOf(eventElement)) return false; + final bool isAggregateEvent = _aggregateEventChecker.hasAnnotationOf(eventElement); + final bool isOperationFor = _operationForChecker.hasAnnotationOf(eventElement); + if (!isAggregateEvent && !isOperationFor) return false; - final annotation = _aggregateEventChecker.firstAnnotationOf(eventElement); + final annotation = _operationForChecker.firstAnnotationOf(eventElement) ?? _aggregateEventChecker.firstAnnotationOf(eventElement); if (annotation == null) return false; - final DartType? annotatedAggregateType = annotation.getField('of')?.toTypeValue(); + final String targetFieldName = isOperationFor ? 'type' : 'of'; + final DartType? annotatedAggregateType = annotation.getField(targetFieldName)?.toTypeValue(); if (annotatedAggregateType?.element != aggregateClassElement) return false; return annotation.getField('creation')?.toBoolValue() ?? false; diff --git a/packages/continuum_lints/lib/src/continuum_required_creation_factories.dart b/packages/continuum_lints/lib/src/continuum_required_creation_factories.dart index 1d5824b..9640e98 100644 --- a/packages/continuum_lints/lib/src/continuum_required_creation_factories.dart +++ b/packages/continuum_lints/lib/src/continuum_required_creation_factories.dart @@ -2,20 +2,24 @@ import 'package:analyzer/dart/element/element.dart'; import 'package:analyzer/dart/element/type.dart'; import 'package:custom_lint_builder/custom_lint_builder.dart'; -/// Computes which creation factory methods an aggregate root is missing. +/// Computes which creation factory methods an operation target is missing. /// -/// A creation event `E` for aggregate `A` requires a matching static factory: -/// `static A createFromE(E event)`. +/// A creation operation `O` for target `T` requires a matching static factory: +/// `static T createFromO(O operation)`. final class ContinuumRequiredCreationFactories { static final TypeChecker _aggregateEventChecker = const TypeChecker.fromUrl( 'package:continuum/src/annotations/aggregate_event.dart#AggregateEvent', ); + static final TypeChecker _operationForChecker = const TypeChecker.fromUrl( + 'package:continuum/src/annotations/operation_for.dart#OperationFor', + ); + static final TypeChecker _continuumEventChecker = const TypeChecker.fromUrl( 'package:continuum/src/events/continuum_event.dart#ContinuumEvent', ); - /// Creates a requirement checker for aggregate creation factories. + /// Creates a requirement checker for operation target creation factories. const ContinuumRequiredCreationFactories(); /// Returns the list of missing factory method names. @@ -95,14 +99,17 @@ final class ContinuumRequiredCreationFactories { for (final LibraryElement library in librariesToScan) { for (final ClassElement candidate in library.classes) { - if (!_aggregateEventChecker.hasAnnotationOf(candidate)) continue; + final bool isAggregateEvent = _aggregateEventChecker.hasAnnotationOf(candidate); + final bool isOperationFor = _operationForChecker.hasAnnotationOf(candidate); + if (!isAggregateEvent && !isOperationFor) continue; if (!_continuumEventChecker.isAssignableFrom(candidate)) continue; - final annotation = _aggregateEventChecker.firstAnnotationOf(candidate); + final annotation = _operationForChecker.firstAnnotationOf(candidate) ?? _aggregateEventChecker.firstAnnotationOf(candidate); if (annotation == null) continue; - final ofValue = annotation.getField('of'); - final DartType? aggregateType = ofValue?.toTypeValue(); + final targetFieldName = isOperationFor ? 'type' : 'of'; + final targetValue = annotation.getField(targetFieldName); + final DartType? aggregateType = targetValue?.toTypeValue(); if (aggregateType?.element != aggregateClassElement) continue; diff --git a/packages/continuum_lints/test/continuum_required_creation_factories_test.dart b/packages/continuum_lints/test/continuum_required_creation_factories_test.dart new file mode 100644 index 0000000..54be01a --- /dev/null +++ b/packages/continuum_lints/test/continuum_required_creation_factories_test.dart @@ -0,0 +1,111 @@ +import 'package:analyzer/dart/element/element.dart'; +import 'package:build/build.dart'; +import 'package:build_test/build_test.dart'; +import 'package:continuum_lints/src/continuum_required_creation_factories.dart'; +import 'package:test/test.dart'; + +void main() { + group('ContinuumRequiredCreationFactories', () { + test('detects missing createFrom factories for OperationFor creation operations', () async { + // Arrange + final Map inputs = { + 'continuum_lints|lib/domain.dart': r''' +import 'package:continuum/continuum.dart'; + +@OperationTarget() +class UserProfile { + UserProfile(); +} + +@OperationFor(type: UserProfile, creation: true) +class UserProfileCreated implements ContinuumEvent { + const UserProfileCreated(); + + @override + EventId get id => const EventId('evt_1'); + + @override + DateTime get occurredOn => DateTime.utc(2020); + + @override + Map get metadata => const {}; +} +''', + }; + + // Act + final List missing = await resolveSources( + inputs, + (Resolver resolver) async { + final LibraryElement library = await _libraryFor(resolver, 'continuum_lints|lib/domain.dart'); + final ClassElement profile = _classNamed(library, 'UserProfile'); + return const ContinuumRequiredCreationFactories().findMissingCreationFactories(profile); + }, + rootPackage: 'continuum_lints', + readAllSourcesFromFilesystem: true, + ); + + // Assert + // WHY: Creation operations must have a matching static createFrom factory. + expect(missing, contains('createFromUserProfileCreated')); + }); + + test('does not report missing factory when createFrom factory exists', () async { + // Arrange + final Map inputs = { + 'continuum_lints|lib/domain.dart': r''' +import 'package:continuum/continuum.dart'; + +@OperationTarget() +class UserProfile { + UserProfile(); + + static UserProfile createFromUserProfileCreated(UserProfileCreated event) { + return UserProfile(); + } +} + +@OperationFor(type: UserProfile, creation: true) +class UserProfileCreated implements ContinuumEvent { + const UserProfileCreated(); + + @override + EventId get id => const EventId('evt_1'); + + @override + DateTime get occurredOn => DateTime.utc(2020); + + @override + Map get metadata => const {}; +} +''', + }; + + // Act + final List missing = await resolveSources( + inputs, + (Resolver resolver) async { + final LibraryElement library = await _libraryFor(resolver, 'continuum_lints|lib/domain.dart'); + final ClassElement profile = _classNamed(library, 'UserProfile'); + return const ContinuumRequiredCreationFactories().findMissingCreationFactories(profile); + }, + rootPackage: 'continuum_lints', + readAllSourcesFromFilesystem: true, + ); + + // Assert + expect(missing, isEmpty); + }); + }); +} + +Future _libraryFor(Resolver resolver, String assetId) async { + return resolver.libraryFor(AssetId.parse(assetId)); +} + +ClassElement _classNamed(LibraryElement library, String name) { + for (final ClassElement element in library.classes) { + if (element.displayName == name) return element; + } + throw StateError('Class not found: $name'); +} diff --git a/packages/continuum_state/README.md b/packages/continuum_state/README.md index c9ef5a1..10875ff 100644 --- a/packages/continuum_state/README.md +++ b/packages/continuum_state/README.md @@ -1,6 +1,6 @@ # Continuum State -State-based persistence strategy for [Continuum](https://github.com/zooper-lib/continuum). Provides adapter-driven aggregate persistence for backends that store full aggregate state (REST APIs, databases, GraphQL) rather than event streams. +State-based persistence strategy for [Continuum](https://github.com/zooper-lib/continuum). Provides adapter-driven target persistence for backends that store full entity state (REST APIs, databases, GraphQL) rather than event streams. ## Installation @@ -18,7 +18,7 @@ dev_dependencies: ### StateBasedStore -The configuration root for state-based persistence. Instead of persisting events, each aggregate is loaded and saved through an `AggregatePersistenceAdapter`. +The configuration root for state-based persistence. Instead of persisting events, each target is loaded and saved through a `TargetPersistenceAdapter`. ```dart import 'package:continuum_state/continuum_state.dart'; @@ -26,7 +26,7 @@ import 'continuum.g.dart'; final store = StateBasedStore( adapters: {User: UserApiAdapter(httpClient)}, - aggregates: $aggregateList, + targets: $aggregateList, ); final session = store.openSession(); @@ -34,12 +34,12 @@ await session.applyAsync(userId, UserRegistered(...)); await session.saveChangesAsync(); // Adapter persists to backend ``` -### AggregatePersistenceAdapter +### TargetPersistenceAdapter -Each adapter implements two methods — `fetchAsync` to load an aggregate and `persistAsync` to save it: +Each adapter implements two methods — `fetchAsync` to load a target and `persistAsync` to save it: ```dart -class UserApiAdapter implements AggregatePersistenceAdapter { +class UserApiAdapter implements TargetPersistenceAdapter { final HttpClient _client; UserApiAdapter(this._client); @@ -53,12 +53,15 @@ class UserApiAdapter implements AggregatePersistenceAdapter { @override Future persistAsync( StreamId streamId, - User aggregate, - List pendingEvents, + User target, + List pendingOperations, ) async { - await _client.put('/users/${streamId.value}', body: aggregate.toJson()); + await _client.put('/users/${streamId.value}', body: target.toJson()); } } + +> Note: `AggregatePersistenceAdapter` is kept as a deprecated alias for backward +> compatibility. ``` ### Exceptions diff --git a/packages/continuum_state/lib/continuum_state.dart b/packages/continuum_state/lib/continuum_state.dart index 42fc56a..42ec568 100644 --- a/packages/continuum_state/lib/continuum_state.dart +++ b/packages/continuum_state/lib/continuum_state.dart @@ -1,7 +1,7 @@ /// Continuum State — State-based persistence strategy. /// -/// Provides adapter-driven aggregate persistence for backends that -/// store full aggregate state (REST APIs, databases, GraphQL) rather +/// Provides adapter-driven target persistence for backends that +/// store full entity state (REST APIs, databases, GraphQL) rather /// than event streams. Reuses the shared session lifecycle from /// `continuum_uow`. library; @@ -9,5 +9,6 @@ library; export 'src/exceptions/permanent_adapter_exception.dart'; export 'src/exceptions/transient_adapter_exception.dart'; export 'src/persistence/aggregate_persistence_adapter.dart'; +export 'src/persistence/target_persistence_adapter.dart'; export 'src/persistence/state_based_session.dart'; export 'src/persistence/state_based_store.dart'; diff --git a/packages/continuum_state/lib/src/persistence/aggregate_persistence_adapter.dart b/packages/continuum_state/lib/src/persistence/aggregate_persistence_adapter.dart index c78f66c..dac8b1c 100644 --- a/packages/continuum_state/lib/src/persistence/aggregate_persistence_adapter.dart +++ b/packages/continuum_state/lib/src/persistence/aggregate_persistence_adapter.dart @@ -1,33 +1,8 @@ -import 'package:continuum/continuum.dart'; +import 'package:continuum_state/src/persistence/target_persistence_adapter.dart'; -/// Adapter interface for state-based aggregate persistence. +/// Backward-compatible alias for the old name. /// -/// Provides fetch/hydrate and persist operations against a backend -/// (REST API, GraphQL, database, etc.). Each adapter is typed to a -/// specific aggregate and is responsible for translating between -/// domain objects and backend representations. -abstract interface class AggregatePersistenceAdapter { - /// Loads and fully constructs an aggregate from the backend. - /// - /// The adapter is responsible for calling the backend, receiving the - /// response, and constructing the aggregate. The session does not - /// need to know how hydration works. - /// - /// Throws if the aggregate does not exist or the backend call fails. - Future fetchAsync(StreamId streamId); - - /// Persists changes for an aggregate to the backend. - /// - /// Receives the stream ID, the aggregate in its post-event state, and - /// the list of pending operations. The adapter decides how to - /// translate these into backend API calls (single PATCH, multiple - /// requests, batch command, etc.). - /// - /// Must be all-or-nothing: either all changes for the given stream - /// are persisted, or the method throws and no changes are committed. - Future persistAsync( - StreamId streamId, - TAggregate aggregate, - List pendingOperations, - ); -} +/// This API used to be named `AggregatePersistenceAdapter` before the +/// operation-target terminology was introduced. +@Deprecated('Use TargetPersistenceAdapter instead.') +typedef AggregatePersistenceAdapter = TargetPersistenceAdapter; diff --git a/packages/continuum_state/lib/src/persistence/state_based_session.dart b/packages/continuum_state/lib/src/persistence/state_based_session.dart index a776f20..7d548f4 100644 --- a/packages/continuum_state/lib/src/persistence/state_based_session.dart +++ b/packages/continuum_state/lib/src/persistence/state_based_session.dart @@ -1,22 +1,21 @@ import 'package:continuum/continuum.dart'; +import 'package:continuum_state/src/persistence/target_persistence_adapter.dart'; import 'package:continuum_uow/continuum_uow.dart'; -import 'aggregate_persistence_adapter.dart'; - -/// Session implementation backed by [AggregatePersistenceAdapter] instances. +/// Session implementation backed by [TargetPersistenceAdapter] instances. /// /// Delegates load to `adapter.fetchAsync` and save to `adapter.persistAsync`, /// reusing the shared operation-application logic from [SessionBase]. Supports /// concurrency retry when adapters throw [ConcurrencyException] and reports /// partial multi-stream save failures via [PartialSaveException]. final class StateBasedSession extends SessionBase { - /// Adapter map keyed by aggregate type. - final Map> _adapters; + /// Adapter map keyed by target type. + final Map> _adapters; /// Creates a state-based session with the given adapter map and /// shared registries. StateBasedSession({ - required Map> adapters, + required Map> adapters, required super.aggregateFactories, required super.eventAppliers, super.applicationMode = EventApplicationMode.eager, @@ -51,7 +50,7 @@ final class StateBasedSession extends SessionBase { @override Future> loadAllAsync() async { // State-based sessions do not support bulk loading — there is no - // event store to query for stream IDs by aggregate type. + // event store to query for stream IDs by target type. throw const InvalidOperationException( message: 'loadAllAsync is not supported by StateBasedSession. ' @@ -226,16 +225,16 @@ final class StateBasedSession extends SessionBase { ); } - /// Resolves the adapter for the given aggregate type. + /// Resolves the adapter for the given target type. /// /// Throws [InvalidOperationException] if no adapter is registered /// for [TAggregate]. - AggregatePersistenceAdapter _resolveAdapter() { + TargetPersistenceAdapter _resolveAdapter() { final adapter = _adapters[TAggregate]; if (adapter == null) { throw InvalidOperationException( message: - 'No AggregatePersistenceAdapter registered for $TAggregate. ' + 'No TargetPersistenceAdapter registered for $TAggregate. ' 'Ensure an adapter is provided in the StateBasedStore constructor.', ); } diff --git a/packages/continuum_state/lib/src/persistence/state_based_store.dart b/packages/continuum_state/lib/src/persistence/state_based_store.dart index 3760da7..5555ef3 100644 --- a/packages/continuum_state/lib/src/persistence/state_based_store.dart +++ b/packages/continuum_state/lib/src/persistence/state_based_store.dart @@ -1,22 +1,22 @@ import 'package:continuum/continuum.dart'; +import 'package:continuum_state/src/persistence/target_persistence_adapter.dart'; import 'package:continuum_uow/continuum_uow.dart'; -import 'aggregate_persistence_adapter.dart'; import 'state_based_session.dart'; -/// Store implementation backed by [AggregatePersistenceAdapter] instances. +/// Store implementation backed by [TargetPersistenceAdapter] instances. /// -/// Each aggregate type is mapped to an adapter that handles fetch and +/// Each target type is mapped to an adapter that handles fetch and /// persist operations against a backend (REST API, GraphQL, database, /// etc.). Uses the same [GeneratedAggregate] registries as the event -/// sourcing store for event application and aggregate creation. +/// sourcing store for event application and entity creation. /// /// Does not depend on event store, event serializer, or serializer /// registry — events exist only in memory as domain objects and are /// never serialized. final class StateBasedStore implements SessionStore { - /// Adapter map keyed by aggregate type. - final Map> _adapters; + /// Adapter map keyed by target type. + final Map> _adapters; /// Aggregate factory registry for creating instances from events. final AggregateFactoryRegistry _aggregateFactories; @@ -27,10 +27,10 @@ final class StateBasedStore implements SessionStore { /// Controls when events mutate the in-memory aggregate. final EventApplicationMode _applicationMode; - /// Creates a state-based store from adapter map and generated aggregate + /// Creates a state-based store from adapter map and generated target /// bundles. /// - /// Pass adapters keyed by aggregate type and all generated aggregate + /// Pass adapters keyed by target type and all generated aggregate /// bundles (e.g., `$User`, `$Account`). The store merges their /// factory and applier registries automatically. Serializer registries /// from [GeneratedAggregate] are ignored — events are never serialized @@ -39,19 +39,32 @@ final class StateBasedStore implements SessionStore { /// Optionally provide an [applicationMode] to control when events /// mutate the in-memory aggregate. Defaults to [EventApplicationMode.eager]. factory StateBasedStore({ - required Map> adapters, - required List aggregates, + required Map> adapters, + List? targets, + @Deprecated('Use targets instead. This alias will be removed in a future major release.') List? aggregates, EventApplicationMode applicationMode = EventApplicationMode.eager, }) { + final List? resolvedTargets = targets ?? aggregates; + if (resolvedTargets == null) { + throw ArgumentError( + 'StateBasedStore requires either `targets` or `aggregates`.', + ); + } + if (targets != null && aggregates != null) { + throw ArgumentError( + 'Provide only one of `targets` or `aggregates`.', + ); + } + // Merge factory and applier registries from all provided aggregates. // Same pattern as the event sourcing store — serializer registries // are ignored because state-based mode never serializes events. var aggregateFactories = const AggregateFactoryRegistry.empty(); var eventAppliers = const EventApplierRegistry.empty(); - for (final aggregate in aggregates) { - aggregateFactories = aggregateFactories.merge(aggregate.aggregateFactories); - eventAppliers = eventAppliers.merge(aggregate.eventAppliers); + for (final target in resolvedTargets) { + aggregateFactories = aggregateFactories.merge(target.aggregateFactories); + eventAppliers = eventAppliers.merge(target.eventAppliers); } return StateBasedStore._( @@ -64,7 +77,7 @@ final class StateBasedStore implements SessionStore { /// Creates a state-based store with explicit dependencies. StateBasedStore._({ - required Map> adapters, + required Map> adapters, required AggregateFactoryRegistry aggregateFactories, required EventApplierRegistry eventAppliers, required EventApplicationMode applicationMode, diff --git a/packages/continuum_state/lib/src/persistence/target_persistence_adapter.dart b/packages/continuum_state/lib/src/persistence/target_persistence_adapter.dart new file mode 100644 index 0000000..ea1c74a --- /dev/null +++ b/packages/continuum_state/lib/src/persistence/target_persistence_adapter.dart @@ -0,0 +1,33 @@ +import 'package:continuum/continuum.dart'; + +/// Adapter interface for state-based target persistence. +/// +/// Provides fetch/hydrate and persist operations against a backend +/// (REST API, GraphQL, database, etc.). Each adapter is typed to a +/// specific target and is responsible for translating between +/// domain objects and backend representations. +abstract interface class TargetPersistenceAdapter { + /// Loads and fully constructs a target from the backend. + /// + /// The adapter is responsible for calling the backend, receiving the + /// response, and constructing the target. The session does not + /// need to know how hydration works. + /// + /// Throws if the target does not exist or the backend call fails. + Future fetchAsync(StreamId streamId); + + /// Persists changes for a target to the backend. + /// + /// Receives the stream ID, the target in its post-operation state, and + /// the list of pending operations. The adapter decides how to + /// translate these into backend API calls (single PATCH, multiple + /// requests, batch command, etc.). + /// + /// Must be all-or-nothing: either all changes for the given stream + /// are persisted, or the method throws and no changes are committed. + Future persistAsync( + StreamId streamId, + TTarget target, + List pendingOperations, + ); +} \ No newline at end of file diff --git a/packages/continuum_state/test/persistence/state_based_session_test.dart b/packages/continuum_state/test/persistence/state_based_session_test.dart index 7f4d233..101094f 100644 --- a/packages/continuum_state/test/persistence/state_based_session_test.dart +++ b/packages/continuum_state/test/persistence/state_based_session_test.dart @@ -8,25 +8,25 @@ import 'package:test/test.dart'; import '../_fixtures/counter_fixtures.dart'; @GenerateNiceMocks([ - MockSpec>(), + MockSpec>(), ]) import 'state_based_session_test.mocks.dart'; void main() { - late MockAggregatePersistenceAdapter mockAdapter; + late MockTargetPersistenceAdapter mockAdapter; late AggregateFactoryRegistry factoryRegistry; late EventApplierRegistry applierRegistry; setUp(() { provideDummy(Counter(0)); - mockAdapter = MockAggregatePersistenceAdapter(); + mockAdapter = MockTargetPersistenceAdapter(); factoryRegistry = buildCounterFactoryRegistry(); applierRegistry = buildCounterApplierRegistry(); }); /// Creates a [StateBasedSession] with the given adapter map and mode. StateBasedSession createSession({ - Map>? adapters, + Map>? adapters, EventApplicationMode applicationMode = EventApplicationMode.eager, }) { return StateBasedSession( diff --git a/packages/continuum_state/test/persistence/state_based_session_test.mocks.dart b/packages/continuum_state/test/persistence/state_based_session_test.mocks.dart index 20972a7..4b1e324 100644 --- a/packages/continuum_state/test/persistence/state_based_session_test.mocks.dart +++ b/packages/continuum_state/test/persistence/state_based_session_test.mocks.dart @@ -6,8 +6,7 @@ import 'dart:async' as _i4; import 'package:continuum/continuum.dart' as _i5; -import 'package:continuum_state/src/persistence/aggregate_persistence_adapter.dart' - as _i2; +import 'package:continuum_state/src/persistence/target_persistence_adapter.dart' as _i2; import 'package:mockito/mockito.dart' as _i1; import 'package:mockito/src/dummies.dart' as _i6; @@ -28,11 +27,10 @@ import '../_fixtures/counter_fixtures.dart' as _i3; // ignore_for_file: subtype_of_sealed_class // ignore_for_file: invalid_use_of_internal_member -/// A class which mocks [AggregatePersistenceAdapter]. +/// A class which mocks [TargetPersistenceAdapter]. /// /// See the documentation for Mockito's code generation for more information. -class MockAggregatePersistenceAdapter extends _i1.Mock - implements _i2.AggregatePersistenceAdapter<_i3.Counter> { +class MockTargetPersistenceAdapter extends _i1.Mock implements _i2.TargetPersistenceAdapter<_i3.Counter> { @override _i4.Future<_i3.Counter> fetchAsync(_i5.StreamId? streamId) => (super.noSuchMethod( diff --git a/packages/continuum_state/test/persistence/state_based_store_test.dart b/packages/continuum_state/test/persistence/state_based_store_test.dart index fe2a738..26162e9 100644 --- a/packages/continuum_state/test/persistence/state_based_store_test.dart +++ b/packages/continuum_state/test/persistence/state_based_store_test.dart @@ -8,17 +8,17 @@ import 'package:test/test.dart'; import '../_fixtures/counter_fixtures.dart'; @GenerateNiceMocks([ - MockSpec>(), + MockSpec>(), ]) import 'state_based_store_test.mocks.dart'; void main() { - late MockAggregatePersistenceAdapter mockAdapter; + late MockTargetPersistenceAdapter mockAdapter; late GeneratedAggregate generatedAggregate; setUp(() { provideDummy(Counter(0)); - mockAdapter = MockAggregatePersistenceAdapter(); + mockAdapter = MockTargetPersistenceAdapter(); generatedAggregate = buildGeneratedCounterAggregate(); }); @@ -26,7 +26,7 @@ void main() { // Arrange & Act final store = StateBasedStore( adapters: {Counter: mockAdapter}, - aggregates: [generatedAggregate], + targets: [generatedAggregate], ); // Assert — type assignability check. @@ -37,7 +37,7 @@ void main() { // Arrange final store = StateBasedStore( adapters: {Counter: mockAdapter}, - aggregates: [generatedAggregate], + targets: [generatedAggregate], ); // Act @@ -52,7 +52,7 @@ void main() { // Arrange — create store without explicit mode. final store = StateBasedStore( adapters: {Counter: mockAdapter}, - aggregates: [generatedAggregate], + targets: [generatedAggregate], ); final session = store.openSession(); const streamId = StreamId('counter-1'); @@ -75,7 +75,7 @@ void main() { // Arrange — create store with deferred mode. final store = StateBasedStore( adapters: {Counter: mockAdapter}, - aggregates: [generatedAggregate], + targets: [generatedAggregate], applicationMode: EventApplicationMode.deferred, ); final session = store.openSession(); @@ -99,7 +99,7 @@ void main() { // Arrange final store = StateBasedStore( adapters: {Counter: mockAdapter}, - aggregates: [generatedAggregate], + targets: [generatedAggregate], ); final runner = TransactionalRunner(store: store); @@ -127,7 +127,7 @@ void main() { // Arrange final store = StateBasedStore( adapters: {Counter: mockAdapter}, - aggregates: [generatedAggregate], + targets: [generatedAggregate], ); final session = store.openSession(); const streamId = StreamId('counter-1'); diff --git a/packages/continuum_state/test/persistence/state_based_store_test.mocks.dart b/packages/continuum_state/test/persistence/state_based_store_test.mocks.dart index 2cb54ff..9b3d8e9 100644 --- a/packages/continuum_state/test/persistence/state_based_store_test.mocks.dart +++ b/packages/continuum_state/test/persistence/state_based_store_test.mocks.dart @@ -6,8 +6,7 @@ import 'dart:async' as _i4; import 'package:continuum/continuum.dart' as _i5; -import 'package:continuum_state/src/persistence/aggregate_persistence_adapter.dart' - as _i2; +import 'package:continuum_state/continuum_state.dart' as _i2; import 'package:mockito/mockito.dart' as _i1; import 'package:mockito/src/dummies.dart' as _i6; @@ -28,11 +27,10 @@ import '../_fixtures/counter_fixtures.dart' as _i3; // ignore_for_file: subtype_of_sealed_class // ignore_for_file: invalid_use_of_internal_member -/// A class which mocks [AggregatePersistenceAdapter]. +/// A class which mocks [TargetPersistenceAdapter]. /// /// See the documentation for Mockito's code generation for more information. -class MockAggregatePersistenceAdapter extends _i1.Mock - implements _i2.AggregatePersistenceAdapter<_i3.Counter> { +class MockTargetPersistenceAdapter extends _i1.Mock implements _i2.TargetPersistenceAdapter<_i3.Counter> { @override _i4.Future<_i3.Counter> fetchAsync(_i5.StreamId? streamId) => (super.noSuchMethod( diff --git a/packages/continuum_store_hive/README.md b/packages/continuum_store_hive/README.md index 5404b71..9a8945b 100644 --- a/packages/continuum_store_hive/README.md +++ b/packages/continuum_store_hive/README.md @@ -29,7 +29,7 @@ void main() async { final store = EventSourcingStore( eventStore: eventStore, - aggregates: $aggregateList, + targets: $aggregateList, ); // Use your aggregates diff --git a/packages/continuum_store_hive/example/lib/domain/account.dart b/packages/continuum_store_hive/example/lib/domain/account.dart index 7ca1d2a..4acc03d 100644 --- a/packages/continuum_store_hive/example/lib/domain/account.dart +++ b/packages/continuum_store_hive/example/lib/domain/account.dart @@ -18,6 +18,7 @@ final class AccountId extends TypedIdentity { } /// A bank Account aggregate demonstrating multiple aggregates in one project. +@OperationTarget() class Account extends AggregateRoot with _$AccountEventHandlers { final String ownerId; int balance; diff --git a/packages/continuum_store_hive/example/lib/domain/events/account_opened.dart b/packages/continuum_store_hive/example/lib/domain/events/account_opened.dart index a34876a..0a6f03a 100644 --- a/packages/continuum_store_hive/example/lib/domain/events/account_opened.dart +++ b/packages/continuum_store_hive/example/lib/domain/events/account_opened.dart @@ -3,7 +3,7 @@ import 'package:continuum/continuum.dart'; import '../account.dart'; /// Event fired when a new account is opened. -@AggregateEvent(of: Account, type: 'account.opened', creation: true) +@OperationFor(type: Account, key: 'account.opened', creation: true) class AccountOpened implements ContinuumEvent { AccountOpened({ required this.accountId, diff --git a/packages/continuum_store_hive/example/lib/domain/events/email_changed.dart b/packages/continuum_store_hive/example/lib/domain/events/email_changed.dart index 83d09d9..c59fc95 100644 --- a/packages/continuum_store_hive/example/lib/domain/events/email_changed.dart +++ b/packages/continuum_store_hive/example/lib/domain/events/email_changed.dart @@ -3,7 +3,7 @@ import 'package:continuum/continuum.dart'; import '../user.dart'; /// Event fired when a user changes their email address. -@AggregateEvent(of: User, type: 'user.email_changed') +@OperationFor(type: User, key: 'user.email_changed') class EmailChanged implements ContinuumEvent { EmailChanged({ required this.newEmail, diff --git a/packages/continuum_store_hive/example/lib/domain/events/funds_deposited.dart b/packages/continuum_store_hive/example/lib/domain/events/funds_deposited.dart index e390039..9a06034 100644 --- a/packages/continuum_store_hive/example/lib/domain/events/funds_deposited.dart +++ b/packages/continuum_store_hive/example/lib/domain/events/funds_deposited.dart @@ -3,7 +3,7 @@ import 'package:continuum/continuum.dart'; import '../account.dart'; /// Event fired when funds are deposited into an account. -@AggregateEvent(of: Account, type: 'account.funds_deposited') +@OperationFor(type: Account, key: 'account.funds_deposited') class FundsDeposited implements ContinuumEvent { FundsDeposited({ required this.amount, diff --git a/packages/continuum_store_hive/example/lib/domain/events/funds_withdrawn.dart b/packages/continuum_store_hive/example/lib/domain/events/funds_withdrawn.dart index 6f5642c..65166b8 100644 --- a/packages/continuum_store_hive/example/lib/domain/events/funds_withdrawn.dart +++ b/packages/continuum_store_hive/example/lib/domain/events/funds_withdrawn.dart @@ -3,8 +3,9 @@ import 'package:continuum/continuum.dart'; import '../account.dart'; /// Event fired when funds are withdrawn from an account. -@AggregateEvent(of: Account, type: 'account.funds_withdrawn') +@OperationFor(type: Account, key: 'account.funds_withdrawn') class FundsWithdrawn implements ContinuumEvent { + FundsWithdrawn({ required this.amount, EventId? eventId, diff --git a/packages/continuum_store_hive/example/lib/domain/events/user_deactivated.dart b/packages/continuum_store_hive/example/lib/domain/events/user_deactivated.dart index 2182d58..2dd9851 100644 --- a/packages/continuum_store_hive/example/lib/domain/events/user_deactivated.dart +++ b/packages/continuum_store_hive/example/lib/domain/events/user_deactivated.dart @@ -3,7 +3,7 @@ import 'package:continuum/continuum.dart'; import '../user.dart'; /// Event fired when a user account is deactivated. -@AggregateEvent(of: User, type: 'user.deactivated') +@OperationFor(type: User, key: 'user.deactivated') class UserDeactivated implements ContinuumEvent { UserDeactivated({ required this.deactivatedAt, diff --git a/packages/continuum_store_hive/example/lib/domain/events/user_registered.dart b/packages/continuum_store_hive/example/lib/domain/events/user_registered.dart index 33f43d2..ce308cd 100644 --- a/packages/continuum_store_hive/example/lib/domain/events/user_registered.dart +++ b/packages/continuum_store_hive/example/lib/domain/events/user_registered.dart @@ -3,7 +3,7 @@ import 'package:continuum/continuum.dart'; import '../user.dart'; /// Event fired when a new user registers. -@AggregateEvent(of: User, type: 'user.registered', creation: true) +@OperationFor(type: User, key: 'user.registered', creation: true) class UserRegistered implements ContinuumEvent { UserRegistered({ required this.userId, diff --git a/packages/continuum_store_hive/example/lib/domain/user.dart b/packages/continuum_store_hive/example/lib/domain/user.dart index dc5b947..9ded3c2 100644 --- a/packages/continuum_store_hive/example/lib/domain/user.dart +++ b/packages/continuum_store_hive/example/lib/domain/user.dart @@ -18,6 +18,7 @@ final class UserId extends TypedIdentity { } /// A User aggregate demonstrating event sourcing with Hive persistence. +@OperationTarget() class User extends AggregateRoot with _$UserEventHandlers { String email; String name; diff --git a/packages/continuum_store_hive/example/main.dart b/packages/continuum_store_hive/example/main.dart index bbfbb17..c2558b6 100644 --- a/packages/continuum_store_hive/example/main.dart +++ b/packages/continuum_store_hive/example/main.dart @@ -42,7 +42,7 @@ void main() async { final hiveStore = await HiveEventStore.openAsync(boxName: 'events'); final store = EventSourcingStore( eventStore: hiveStore, - aggregates: $aggregateList, // Auto-generated from AggregateRoot classes + targets: $aggregateList, // Auto-generated from @OperationTarget / legacy AggregateRoot targets ); print('Creating a user...'); diff --git a/packages/continuum_store_hive/lib/continuum_store_hive.dart b/packages/continuum_store_hive/lib/continuum_store_hive.dart index d10fd1f..6b6ccba 100644 --- a/packages/continuum_store_hive/lib/continuum_store_hive.dart +++ b/packages/continuum_store_hive/lib/continuum_store_hive.dart @@ -1,7 +1,7 @@ /// Hive-backed store implementations for the continuum library. /// /// Provides persistent local [EventStore], [ReadModelStore], and -/// [AggregatePersistenceAdapter] implementations using Hive. +/// [TargetPersistenceAdapter] implementations using Hive. library; export 'src/hive_event_store.dart'; diff --git a/packages/continuum_store_hive/lib/src/hive_persistence_adapter.dart b/packages/continuum_store_hive/lib/src/hive_persistence_adapter.dart index 2234013..1a09c98 100644 --- a/packages/continuum_store_hive/lib/src/hive_persistence_adapter.dart +++ b/packages/continuum_store_hive/lib/src/hive_persistence_adapter.dart @@ -4,22 +4,22 @@ import 'package:continuum/continuum.dart'; import 'package:continuum_state/continuum_state.dart'; import 'package:hive/hive.dart'; -/// Hive-backed implementation of [AggregatePersistenceAdapter]. +/// Hive-backed implementation of [TargetPersistenceAdapter]. /// -/// Stores aggregates as JSON strings in a Hive [Box]. -/// Aggregates survive app restarts and device reboots. +/// Stores targets as JSON strings in a Hive [Box]. +/// Targets survive app restarts and device reboots. /// /// The caller provides [toJson] and [fromJson] callbacks for -/// aggregate serialization. The adapter stores the full entity +/// target serialization. The adapter stores the full entity /// state on every persist — pending operations are ignored. -final class HivePersistenceAdapter implements AggregatePersistenceAdapter { - /// The Hive box storing JSON-encoded aggregates. +final class HivePersistenceAdapter implements TargetPersistenceAdapter { + /// The Hive box storing JSON-encoded targets. final Box _box; - /// Serializes an aggregate to a JSON-compatible map. - final Map Function(TAggregate aggregate) _toJson; + /// Serializes a target to a JSON-compatible map. + final Map Function(TAggregate target) _toJson; - /// Deserializes an aggregate from a JSON-compatible map. + /// Deserializes a target from a JSON-compatible map. final TAggregate Function(Map json) _fromJson; /// Private constructor — use [openAsync] factory. @@ -35,9 +35,9 @@ final class HivePersistenceAdapter implements AggregatePersistenceAd /// /// If the box already exists, it is reopened with existing data. /// - /// The [toJson] and [fromJson] callbacks handle aggregate + /// The [toJson] and [fromJson] callbacks handle target /// serialization. Both callbacks are responsible for mapping all - /// mutable aggregate state — not just the creation fields. + /// mutable state — not just the creation fields. static Future> openAsync({ required String boxName, required Map Function(TAggregate aggregate) toJson, @@ -56,11 +56,11 @@ final class HivePersistenceAdapter implements AggregatePersistenceAd final json = _box.get(streamId.value); if (json == null) { throw StateError( - 'Aggregate not found in Hive store: ${streamId.value}', + 'Target not found in Hive store: ${streamId.value}', ); } - // Decode the stored JSON string back into an aggregate. + // Decode the stored JSON string back into a target. final decoded = jsonDecode(json) as Map; return _fromJson(decoded); } @@ -68,13 +68,13 @@ final class HivePersistenceAdapter implements AggregatePersistenceAd @override Future persistAsync( StreamId streamId, - TAggregate aggregate, + TAggregate target, List pendingOperations, ) async { - // Encode the full aggregate state to a JSON string and store it. + // Encode the full target state to a JSON string and store it. // Pending operations are ignored — we always write the complete // entity. - final json = jsonEncode(_toJson(aggregate)); + final json = jsonEncode(_toJson(target)); await _box.put(streamId.value, json); } @@ -85,6 +85,6 @@ final class HivePersistenceAdapter implements AggregatePersistenceAd await _box.close(); } - /// The number of stored aggregates. + /// The number of stored targets. int get length => _box.length; } diff --git a/packages/continuum_store_memory/README.md b/packages/continuum_store_memory/README.md index 574209b..d8e2737 100644 --- a/packages/continuum_store_memory/README.md +++ b/packages/continuum_store_memory/README.md @@ -21,7 +21,7 @@ import 'continuum.g.dart'; // Generated void main() async { final store = EventSourcingStore( eventStore: InMemoryEventStore(), - aggregates: $aggregateList, + targets: $aggregateList, ); // Use your aggregates diff --git a/packages/continuum_store_memory/example/lib/domain/account.dart b/packages/continuum_store_memory/example/lib/domain/account.dart index 5ba91be..b594591 100644 --- a/packages/continuum_store_memory/example/lib/domain/account.dart +++ b/packages/continuum_store_memory/example/lib/domain/account.dart @@ -18,6 +18,7 @@ final class AccountId extends TypedIdentity { } /// A bank Account aggregate demonstrating multiple aggregates in one project. +@OperationTarget() class Account extends AggregateRoot with _$AccountEventHandlers { final String ownerId; int balance; diff --git a/packages/continuum_store_memory/example/lib/domain/events/account_opened.dart b/packages/continuum_store_memory/example/lib/domain/events/account_opened.dart index a34876a..0a6f03a 100644 --- a/packages/continuum_store_memory/example/lib/domain/events/account_opened.dart +++ b/packages/continuum_store_memory/example/lib/domain/events/account_opened.dart @@ -3,7 +3,7 @@ import 'package:continuum/continuum.dart'; import '../account.dart'; /// Event fired when a new account is opened. -@AggregateEvent(of: Account, type: 'account.opened', creation: true) +@OperationFor(type: Account, key: 'account.opened', creation: true) class AccountOpened implements ContinuumEvent { AccountOpened({ required this.accountId, diff --git a/packages/continuum_store_memory/example/lib/domain/events/email_changed.dart b/packages/continuum_store_memory/example/lib/domain/events/email_changed.dart index f3e2447..e60d423 100644 --- a/packages/continuum_store_memory/example/lib/domain/events/email_changed.dart +++ b/packages/continuum_store_memory/example/lib/domain/events/email_changed.dart @@ -3,7 +3,7 @@ import 'package:continuum/continuum.dart'; import '../user.dart'; /// Event fired when a user changes their email address. -@AggregateEvent(of: User, type: 'user.email_changed') +@OperationFor(type: User, key: 'user.email_changed') class EmailChanged implements ContinuumEvent { EmailChanged({ required this.newEmail, diff --git a/packages/continuum_store_memory/example/lib/domain/events/funds_deposited.dart b/packages/continuum_store_memory/example/lib/domain/events/funds_deposited.dart index e390039..9a06034 100644 --- a/packages/continuum_store_memory/example/lib/domain/events/funds_deposited.dart +++ b/packages/continuum_store_memory/example/lib/domain/events/funds_deposited.dart @@ -3,7 +3,7 @@ import 'package:continuum/continuum.dart'; import '../account.dart'; /// Event fired when funds are deposited into an account. -@AggregateEvent(of: Account, type: 'account.funds_deposited') +@OperationFor(type: Account, key: 'account.funds_deposited') class FundsDeposited implements ContinuumEvent { FundsDeposited({ required this.amount, diff --git a/packages/continuum_store_memory/example/lib/domain/events/funds_withdrawn.dart b/packages/continuum_store_memory/example/lib/domain/events/funds_withdrawn.dart index 6f5642c..4d21f19 100644 --- a/packages/continuum_store_memory/example/lib/domain/events/funds_withdrawn.dart +++ b/packages/continuum_store_memory/example/lib/domain/events/funds_withdrawn.dart @@ -3,7 +3,7 @@ import 'package:continuum/continuum.dart'; import '../account.dart'; /// Event fired when funds are withdrawn from an account. -@AggregateEvent(of: Account, type: 'account.funds_withdrawn') +@OperationFor(type: Account, key: 'account.funds_withdrawn') class FundsWithdrawn implements ContinuumEvent { FundsWithdrawn({ required this.amount, diff --git a/packages/continuum_store_memory/example/lib/domain/events/user_deactivated.dart b/packages/continuum_store_memory/example/lib/domain/events/user_deactivated.dart index 4e7c960..e10b1dd 100644 --- a/packages/continuum_store_memory/example/lib/domain/events/user_deactivated.dart +++ b/packages/continuum_store_memory/example/lib/domain/events/user_deactivated.dart @@ -3,7 +3,7 @@ import 'package:continuum/continuum.dart'; import '../user.dart'; /// Event fired when a user account is deactivated. -@AggregateEvent(of: User, type: 'user.deactivated') +@OperationFor(type: User, key: 'user.deactivated') class UserDeactivated implements ContinuumEvent { UserDeactivated({ required this.deactivatedAt, diff --git a/packages/continuum_store_memory/example/lib/domain/events/user_registered.dart b/packages/continuum_store_memory/example/lib/domain/events/user_registered.dart index 6191f93..fea105c 100644 --- a/packages/continuum_store_memory/example/lib/domain/events/user_registered.dart +++ b/packages/continuum_store_memory/example/lib/domain/events/user_registered.dart @@ -3,7 +3,7 @@ import 'package:continuum/continuum.dart'; import '../user.dart'; /// Event fired when a new user registers. -@AggregateEvent(of: User, type: 'user.registered', creation: true) +@OperationFor(type: User, key: 'user.registered', creation: true) class UserRegistered implements ContinuumEvent { UserRegistered({ required this.userId, diff --git a/packages/continuum_store_memory/example/lib/domain/user.dart b/packages/continuum_store_memory/example/lib/domain/user.dart index 102b8df..ac2fcfa 100644 --- a/packages/continuum_store_memory/example/lib/domain/user.dart +++ b/packages/continuum_store_memory/example/lib/domain/user.dart @@ -18,6 +18,7 @@ final class UserId extends TypedIdentity { } /// A User aggregate demonstrating event sourcing with in-memory persistence. +@OperationTarget() class User extends AggregateRoot with _$UserEventHandlers { String email; String name; diff --git a/packages/continuum_store_memory/example/main.dart b/packages/continuum_store_memory/example/main.dart index 6f302dd..1f4b238 100644 --- a/packages/continuum_store_memory/example/main.dart +++ b/packages/continuum_store_memory/example/main.dart @@ -33,7 +33,7 @@ void main() async { // Events are stored in memory only - lost when the process exits final store = EventSourcingStore( eventStore: InMemoryEventStore(), - aggregates: $aggregateList, // Auto-generated from AggregateRoot classes + targets: $aggregateList, // Auto-generated from @OperationTarget / legacy AggregateRoot targets ); print('Creating a user...'); diff --git a/packages/continuum_store_memory/lib/continuum_store_memory.dart b/packages/continuum_store_memory/lib/continuum_store_memory.dart index 93ff9ce..01f899c 100644 --- a/packages/continuum_store_memory/lib/continuum_store_memory.dart +++ b/packages/continuum_store_memory/lib/continuum_store_memory.dart @@ -1,7 +1,7 @@ /// In-memory store implementations for the continuum library. /// /// Provides simple in-memory [EventStore], [ReadModelStore], and -/// [AggregatePersistenceAdapter] implementations suitable for +/// [TargetPersistenceAdapter] implementations suitable for /// testing and development. library; diff --git a/packages/continuum_store_memory/lib/src/in_memory_persistence_adapter.dart b/packages/continuum_store_memory/lib/src/in_memory_persistence_adapter.dart index 46141ad..63d77f2 100644 --- a/packages/continuum_store_memory/lib/src/in_memory_persistence_adapter.dart +++ b/packages/continuum_store_memory/lib/src/in_memory_persistence_adapter.dart @@ -1,34 +1,34 @@ import 'package:continuum/continuum.dart'; import 'package:continuum_state/continuum_state.dart'; -/// In-memory implementation of [AggregatePersistenceAdapter]. +/// In-memory implementation of [TargetPersistenceAdapter]. /// -/// Stores aggregates as JSON maps in a plain [Map], suitable for +/// Stores targets as JSON maps in a plain [Map], suitable for /// testing and development. Data is lost when the instance is /// garbage collected. /// /// The caller provides [toJson] and [fromJson] callbacks for -/// aggregate serialization. The [toJson] callback converts the -/// aggregate to a JSON-compatible map, and [fromJson] reconstructs +/// target serialization. The [toJson] callback converts the +/// target to a JSON-compatible map, and [fromJson] reconstructs /// it from the stored map. -final class InMemoryPersistenceAdapter implements AggregatePersistenceAdapter { +final class InMemoryPersistenceAdapter implements TargetPersistenceAdapter { /// Internal storage keyed by stream ID value. final Map> _storage = {}; - /// Serializes an aggregate to a JSON-compatible map. - final Map Function(TAggregate aggregate) _toJson; + /// Serializes a target to a JSON-compatible map. + final Map Function(TAggregate target) _toJson; - /// Deserializes an aggregate from a JSON-compatible map. + /// Deserializes a target from a JSON-compatible map. final TAggregate Function(Map json) _fromJson; /// Creates an in-memory persistence adapter. /// - /// The [toJson] callback converts the aggregate to a JSON map for - /// storage. The [fromJson] callback reconstructs the aggregate from + /// The [toJson] callback converts the target to a JSON map for + /// storage. The [fromJson] callback reconstructs the target from /// the stored JSON map. Both callbacks are responsible for mapping - /// all mutable aggregate state — not just the creation fields. + /// all mutable state — not just the creation fields. InMemoryPersistenceAdapter({ - required Map Function(TAggregate aggregate) toJson, + required Map Function(TAggregate target) toJson, required TAggregate Function(Map json) fromJson, }) : _toJson = toJson, _fromJson = fromJson; @@ -38,28 +38,28 @@ final class InMemoryPersistenceAdapter implements AggregatePersisten final json = _storage[streamId.value]; if (json == null) { throw StateError( - 'Aggregate not found in in-memory store: ${streamId.value}', + 'Target not found in in-memory store: ${streamId.value}', ); } - // Reconstruct the aggregate from the stored JSON map. + // Reconstruct the target from the stored JSON map. return _fromJson(json); } @override Future persistAsync( StreamId streamId, - TAggregate aggregate, + TAggregate target, List pendingOperations, ) async { - // Store the full aggregate state as a JSON map. The pending + // Store the full target state as a JSON map. The pending // operations are ignored — we always write the complete entity. - _storage[streamId.value] = _toJson(aggregate); + _storage[streamId.value] = _toJson(target); } - /// The number of stored aggregates. + /// The number of stored targets. int get length => _storage.length; - /// Clears all stored aggregates. + /// Clears all stored targets. void clear() => _storage.clear(); } diff --git a/packages/continuum_store_memory/test/session_concurrency_test.dart b/packages/continuum_store_memory/test/session_concurrency_test.dart index 1d5fa85..7f18c4f 100644 --- a/packages/continuum_store_memory/test/session_concurrency_test.dart +++ b/packages/continuum_store_memory/test/session_concurrency_test.dart @@ -109,7 +109,7 @@ void main() { eventStore = InMemoryEventStore(); store = EventSourcingStore( eventStore: eventStore, - aggregates: [_buildCounterAggregate()], + targets: [_buildCounterAggregate()], ); }); diff --git a/packages/continuum_store_memory/test/session_load_all_integration_test.dart b/packages/continuum_store_memory/test/session_load_all_integration_test.dart index ffae639..01a0d91 100644 --- a/packages/continuum_store_memory/test/session_load_all_integration_test.dart +++ b/packages/continuum_store_memory/test/session_load_all_integration_test.dart @@ -164,7 +164,7 @@ void main() { eventStore = InMemoryEventStore(); store = EventSourcingStore( eventStore: eventStore, - aggregates: [_buildCounterAggregate(), _buildWalletAggregate()], + targets: [_buildCounterAggregate(), _buildWalletAggregate()], ); }); diff --git a/packages/continuum_store_sembast/README.md b/packages/continuum_store_sembast/README.md index f0b63d7..95bef63 100644 --- a/packages/continuum_store_sembast/README.md +++ b/packages/continuum_store_sembast/README.md @@ -29,7 +29,7 @@ void main() async { final store = EventSourcingStore( eventStore: eventStore, - aggregates: $aggregateList, + targets: $aggregateList, ); // Use your aggregates diff --git a/packages/continuum_store_sembast/example/lib/domain/account.dart b/packages/continuum_store_sembast/example/lib/domain/account.dart index 7ca1d2a..4acc03d 100644 --- a/packages/continuum_store_sembast/example/lib/domain/account.dart +++ b/packages/continuum_store_sembast/example/lib/domain/account.dart @@ -18,6 +18,7 @@ final class AccountId extends TypedIdentity { } /// A bank Account aggregate demonstrating multiple aggregates in one project. +@OperationTarget() class Account extends AggregateRoot with _$AccountEventHandlers { final String ownerId; int balance; diff --git a/packages/continuum_store_sembast/example/lib/domain/events/account_opened.dart b/packages/continuum_store_sembast/example/lib/domain/events/account_opened.dart index a34876a..0a6f03a 100644 --- a/packages/continuum_store_sembast/example/lib/domain/events/account_opened.dart +++ b/packages/continuum_store_sembast/example/lib/domain/events/account_opened.dart @@ -3,7 +3,7 @@ import 'package:continuum/continuum.dart'; import '../account.dart'; /// Event fired when a new account is opened. -@AggregateEvent(of: Account, type: 'account.opened', creation: true) +@OperationFor(type: Account, key: 'account.opened', creation: true) class AccountOpened implements ContinuumEvent { AccountOpened({ required this.accountId, diff --git a/packages/continuum_store_sembast/example/lib/domain/events/email_changed.dart b/packages/continuum_store_sembast/example/lib/domain/events/email_changed.dart index 83d09d9..c59fc95 100644 --- a/packages/continuum_store_sembast/example/lib/domain/events/email_changed.dart +++ b/packages/continuum_store_sembast/example/lib/domain/events/email_changed.dart @@ -3,7 +3,7 @@ import 'package:continuum/continuum.dart'; import '../user.dart'; /// Event fired when a user changes their email address. -@AggregateEvent(of: User, type: 'user.email_changed') +@OperationFor(type: User, key: 'user.email_changed') class EmailChanged implements ContinuumEvent { EmailChanged({ required this.newEmail, diff --git a/packages/continuum_store_sembast/example/lib/domain/events/funds_deposited.dart b/packages/continuum_store_sembast/example/lib/domain/events/funds_deposited.dart index e390039..9a06034 100644 --- a/packages/continuum_store_sembast/example/lib/domain/events/funds_deposited.dart +++ b/packages/continuum_store_sembast/example/lib/domain/events/funds_deposited.dart @@ -3,7 +3,7 @@ import 'package:continuum/continuum.dart'; import '../account.dart'; /// Event fired when funds are deposited into an account. -@AggregateEvent(of: Account, type: 'account.funds_deposited') +@OperationFor(type: Account, key: 'account.funds_deposited') class FundsDeposited implements ContinuumEvent { FundsDeposited({ required this.amount, diff --git a/packages/continuum_store_sembast/example/lib/domain/events/funds_withdrawn.dart b/packages/continuum_store_sembast/example/lib/domain/events/funds_withdrawn.dart index 6f5642c..4d21f19 100644 --- a/packages/continuum_store_sembast/example/lib/domain/events/funds_withdrawn.dart +++ b/packages/continuum_store_sembast/example/lib/domain/events/funds_withdrawn.dart @@ -3,7 +3,7 @@ import 'package:continuum/continuum.dart'; import '../account.dart'; /// Event fired when funds are withdrawn from an account. -@AggregateEvent(of: Account, type: 'account.funds_withdrawn') +@OperationFor(type: Account, key: 'account.funds_withdrawn') class FundsWithdrawn implements ContinuumEvent { FundsWithdrawn({ required this.amount, diff --git a/packages/continuum_store_sembast/example/lib/domain/events/user_deactivated.dart b/packages/continuum_store_sembast/example/lib/domain/events/user_deactivated.dart index 2182d58..2dd9851 100644 --- a/packages/continuum_store_sembast/example/lib/domain/events/user_deactivated.dart +++ b/packages/continuum_store_sembast/example/lib/domain/events/user_deactivated.dart @@ -3,7 +3,7 @@ import 'package:continuum/continuum.dart'; import '../user.dart'; /// Event fired when a user account is deactivated. -@AggregateEvent(of: User, type: 'user.deactivated') +@OperationFor(type: User, key: 'user.deactivated') class UserDeactivated implements ContinuumEvent { UserDeactivated({ required this.deactivatedAt, diff --git a/packages/continuum_store_sembast/example/lib/domain/events/user_registered.dart b/packages/continuum_store_sembast/example/lib/domain/events/user_registered.dart index 33f43d2..ce308cd 100644 --- a/packages/continuum_store_sembast/example/lib/domain/events/user_registered.dart +++ b/packages/continuum_store_sembast/example/lib/domain/events/user_registered.dart @@ -3,7 +3,7 @@ import 'package:continuum/continuum.dart'; import '../user.dart'; /// Event fired when a new user registers. -@AggregateEvent(of: User, type: 'user.registered', creation: true) +@OperationFor(type: User, key: 'user.registered', creation: true) class UserRegistered implements ContinuumEvent { UserRegistered({ required this.userId, diff --git a/packages/continuum_store_sembast/example/lib/domain/user.dart b/packages/continuum_store_sembast/example/lib/domain/user.dart index 7ad2de8..3a320e9 100644 --- a/packages/continuum_store_sembast/example/lib/domain/user.dart +++ b/packages/continuum_store_sembast/example/lib/domain/user.dart @@ -18,6 +18,7 @@ final class UserId extends TypedIdentity { } /// A User aggregate demonstrating event sourcing with Sembast persistence. +@OperationTarget() class User extends AggregateRoot with _$UserEventHandlers { String email; String name; diff --git a/packages/continuum_store_sembast/example/main.dart b/packages/continuum_store_sembast/example/main.dart index cf4b21e..180ee53 100644 --- a/packages/continuum_store_sembast/example/main.dart +++ b/packages/continuum_store_sembast/example/main.dart @@ -46,7 +46,7 @@ void main() async { ); final store = EventSourcingStore( eventStore: sembastStore, - aggregates: $aggregateList, // Auto-generated from AggregateRoot classes + targets: $aggregateList, // Auto-generated from @OperationTarget / legacy AggregateRoot targets ); print('Creating a user...'); diff --git a/packages/continuum_store_sembast/lib/continuum_store_sembast.dart b/packages/continuum_store_sembast/lib/continuum_store_sembast.dart index 70acac5..4ad237f 100644 --- a/packages/continuum_store_sembast/lib/continuum_store_sembast.dart +++ b/packages/continuum_store_sembast/lib/continuum_store_sembast.dart @@ -1,7 +1,7 @@ /// Sembast-backed store implementations for the continuum library. /// /// Provides persistent local [EventStore], [ReadModelStore], and -/// [AggregatePersistenceAdapter] implementations using Sembast. +/// [TargetPersistenceAdapter] implementations using Sembast. library; export 'src/sembast_event_store.dart'; diff --git a/packages/continuum_store_sembast/lib/src/sembast_persistence_adapter.dart b/packages/continuum_store_sembast/lib/src/sembast_persistence_adapter.dart index d7325c2..0363c43 100644 --- a/packages/continuum_store_sembast/lib/src/sembast_persistence_adapter.dart +++ b/packages/continuum_store_sembast/lib/src/sembast_persistence_adapter.dart @@ -4,36 +4,36 @@ import 'package:continuum/continuum.dart'; import 'package:continuum_state/continuum_state.dart'; import 'package:sembast/sembast.dart'; -/// Sembast-backed implementation of [AggregatePersistenceAdapter]. +/// Sembast-backed implementation of [TargetPersistenceAdapter]. /// -/// Stores aggregates as JSON strings in a Sembast [StoreRef]. -/// Aggregates survive app restarts and device reboots. +/// Stores targets as JSON strings in a Sembast [StoreRef]. +/// Targets survive app restarts and device reboots. /// /// The caller provides [toJson] and [fromJson] callbacks for -/// aggregate serialization. The adapter stores the full entity +/// target serialization. The adapter stores the full entity /// state on every persist — pending operations are ignored. -final class SembastPersistenceAdapter implements AggregatePersistenceAdapter { +final class SembastPersistenceAdapter implements TargetPersistenceAdapter { /// The Sembast database instance. final Database _database; - /// The Sembast store for JSON-encoded aggregates, keyed by string. + /// The Sembast store for JSON-encoded targets, keyed by string. final StoreRef _store; - /// Serializes an aggregate to a JSON-compatible map. - final Map Function(TAggregate aggregate) _toJson; + /// Serializes a target to a JSON-compatible map. + final Map Function(TAggregate target) _toJson; - /// Deserializes an aggregate from a JSON-compatible map. + /// Deserializes a target from a JSON-compatible map. final TAggregate Function(Map json) _fromJson; /// Creates a Sembast-backed persistence adapter. /// /// The [database] must already be opened by the caller. The /// [storeName] identifies the Sembast store within the database — - /// use a unique name per aggregate type to prevent collisions. + /// use a unique name per target type to prevent collisions. /// - /// The [toJson] and [fromJson] callbacks handle aggregate + /// The [toJson] and [fromJson] callbacks handle target /// serialization. Both callbacks are responsible for mapping all - /// mutable aggregate state — not just the creation fields. + /// mutable state — not just the creation fields. SembastPersistenceAdapter({ required Database database, required String storeName, @@ -49,11 +49,11 @@ final class SembastPersistenceAdapter implements AggregatePersistenc final json = await _store.record(streamId.value).get(_database); if (json == null) { throw StateError( - 'Aggregate not found in Sembast store: ${streamId.value}', + 'Target not found in Sembast store: ${streamId.value}', ); } - // Decode the stored JSON string back into an aggregate. + // Decode the stored JSON string back into a target. final decoded = jsonDecode(json) as Map; return _fromJson(decoded); } @@ -61,17 +61,17 @@ final class SembastPersistenceAdapter implements AggregatePersistenc @override Future persistAsync( StreamId streamId, - TAggregate aggregate, + TAggregate target, List pendingOperations, ) async { - // Encode the full aggregate state to a JSON string and store it. + // Encode the full target state to a JSON string and store it. // Pending operations are ignored — we always write the complete // entity. - final json = jsonEncode(_toJson(aggregate)); + final json = jsonEncode(_toJson(target)); await _store.record(streamId.value).put(_database, json); } - /// Returns the number of stored aggregates. + /// Returns the number of stored targets. Future countAsync() async { return _store.count(_database); }