continuum_generator 5.2.0
continuum_generator: ^5.2.0 copied to clipboard
Code generator for the continuum event sourcing library.
Continuum Generator #
Code generator for the continuum event sourcing library. Automatically generates event handling code and discovers all operation targets in your project.
What It Generates #
Per-Target Files (*.g.dart) #
For each operation target (a class annotated with @OperationTarget(), or a legacy AggregateRoot class), generates:
-
Event handler mixin (
_$YourTypeEventHandlers)- Connects your
applyEventName()methods to events - Type-safe event dispatching
- Connects your
-
Extensions:
applyEvent()- Apply single event to aggregatereplayEvents()- Reconstruct aggregate from event streamcreateFromEvent()- Factory for first event
-
Registries (for persistence):
- Event serialization registry
- Aggregate factory registry
- Event applier registry
Global Discovery (lib/continuum.g.dart) #
Automatically discovers all operation targets and generates:
final List<GeneratedAggregate> $aggregateList = [
$Account,
$User,
// ... all aggregates sorted alphabetically
];
This enables zero-configuration setup:
final store = EventSourcingStore(
eventStore: myStore,
targets: $aggregateList, // Just works!
);
Installation #
Add to your pubspec.yaml:
dependencies:
continuum: latest
dev_dependencies:
build_runner: ^2.4.0
continuum_generator: latest
Usage #
1. Annotate Your Classes #
import 'package:continuum/continuum.dart';
part 'user.g.dart';
@OperationTarget()
class User with _$UserEventHandlers {
User._({required this.id, required this.email});
final String id;
String email;
static User createFromUserCreated(UserCreated event) {
return User._(id: event.userId, email: event.email);
}
@override
void applyEmailChanged(EmailChanged event) {
email = event.newEmail;
}
}
@OperationFor(type: User, key: 'user.created', creation: true)
class UserCreated implements ContinuumEvent {
UserCreated({
required this.userId,
required this.email,
EventId? eventId,
DateTime? occurredOn,
Map<String, Object?> metadata = const {},
}) : id = eventId ?? EventId.fromUlid(),
occurredOn = occurredOn ?? DateTime.now(),
metadata = Map<String, Object?>.unmodifiable(metadata);
final String userId;
final String email;
@override
final EventId id;
@override
final DateTime occurredOn;
@override
final Map<String, Object?> metadata;
Map<String, dynamic> toJson() => {
'userId': userId,
'email': email,
'eventId': id.toString(),
'occurredOn': occurredOn.toIso8601String(),
'metadata': metadata,
};
factory UserCreated.fromJson(Map<String, dynamic> json) {
return UserCreated(
userId: json['userId'] as String,
email: json['email'] as String,
eventId: EventId(json['eventId'] as String),
occurredOn: DateTime.parse(json['occurredOn'] as String),
metadata: Map<String, Object?>.from(json['metadata'] as Map),
);
}
}
@OperationFor(type: User, key: 'user.email_changed')
class EmailChanged implements ContinuumEvent {
EmailChanged({
required this.userId,
required this.newEmail,
EventId? eventId,
DateTime? occurredOn,
Map<String, Object?> metadata = const {},
}) : id = eventId ?? EventId.fromUlid(),
occurredOn = occurredOn ?? DateTime.now(),
metadata = Map<String, Object?>.unmodifiable(metadata);
final String userId;
final String newEmail;
@override
final EventId id;
@override
final DateTime occurredOn;
@override
final Map<String, Object?> metadata;
Map<String, dynamic> toJson() => {
'userId': userId,
'newEmail': newEmail,
'eventId': id.toString(),
'occurredOn': occurredOn.toIso8601String(),
'metadata': metadata,
};
factory EmailChanged.fromJson(Map<String, dynamic> json) {
return EmailChanged(
userId: json['userId'] as String,
newEmail: json['newEmail'] as String,
eventId: EventId(json['eventId'] as String),
occurredOn: DateTime.parse(json['occurredOn'] as String),
metadata: Map<String, Object?>.from(json['metadata'] as Map),
);
}
}
2. Run Build Runner #
# One-time build
dart run build_runner build
# Watch mode (rebuilds on file changes)
dart run build_runner watch
# Clean previous builds
dart run build_runner build --delete-conflicting-outputs
3. Use Generated Code #
// Import your aggregate (with generated part)
import 'domain/user.dart';
// Import auto-discovered list
import 'continuum.g.dart';
void main() {
// Zero-configuration setup!
final store = EventSourcingStore(
eventStore: InMemoryEventStore(),
targets: $aggregateList,
);
final userId = StreamId('123');
// Create + mutate within a session
final session = store.openSession();
await session.applyAsync<User>(userId, UserCreated(userId, 'alice@example.com'));
await session.saveChangesAsync();
// Load aggregate (reconstructed from events)
final readSession = store.openSession();
final user = await readSession.loadAsync<User>(userId);
print(user.email); // alice@example.com
}
Generated Code Structure #
Event Handler Mixin #
The generator creates a mixin that dispatches events to your apply methods:
// Generated in user.g.dart
mixin _$UserEventHandlers {
void applyEmailChanged(EmailChanged event);
void $applyEvent(ContinuumEvent event) {
if (event is EmailChanged) return applyEmailChanged(event);
throw UnknownEventException(event.runtimeType);
}
}
Extension Methods #
// Generated in user.g.dart
extension UserEventSourcingExtensions on User {
void applyEvent(ContinuumEvent event) {
$applyEvent(event);
}
static User replayEvents(Iterable<ContinuumEvent> events) {
// Replays events to reconstruct aggregate
}
static User createFromEvent(ContinuumEvent event) {
// Calls User.createFromUserCreated() etc.
}
}
Build Configuration #
Custom Configuration (Optional) #
Create build.yaml in your project root:
targets:
$default:
builders:
continuum_generator:
enabled: true
options:
# Options can be added here in future versions
Multiple Packages #
If you have multiple packages with aggregates, run build_runner in each:
# In package A
cd packages/domain_a
dart run build_runner build
# In package B
cd ../domain_b
dart run build_runner build
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 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
- Maintain a registry
- Merge multiple registries
Just:
- Annotate your target class with
@OperationTarget()(or extendAggregateRootas a legacy marker) - Run
build_runner - Use
$aggregateList
Troubleshooting #
"part 'file.g.dart' not found" #
Run the generator:
dart run build_runner build
"Undefined name '_$MyAggregateEventHandlers'" #
Make sure:
- You have
part 'my_aggregate.g.dart';directive - Your class is an operation target and mixes in
_$MyAggregateEventHandlers: - You've run
build_runner
"No apply method found for event" #
Ensure your aggregate has a method like:
MyAggregate applyMyEvent(MyEvent event) { ... }
The method name must be apply + event class name.
Changes not reflected #
Try rebuilding with clean:
dart run build_runner build --delete-conflicting-outputs
Examples #
See the continuum package examples for complete usage examples.
Contributing #
See the repository for contribution guidelines.
License #
MIT