diff --git a/src/Commands/ReplayCommand.php b/src/Commands/ReplayCommand.php index 2254e7ec..f230f3ee 100644 --- a/src/Commands/ReplayCommand.php +++ b/src/Commands/ReplayCommand.php @@ -8,6 +8,7 @@ use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Event as EventFacade; use Thunk\Verbs\Contracts\BrokersEvents; +use Thunk\Verbs\Contracts\StoresEvents; use Thunk\Verbs\Event; use Thunk\Verbs\Models\VerbEvent; @@ -18,11 +19,11 @@ class ReplayCommand extends Command { - protected $signature = 'verbs:replay {--force}'; + protected $signature = 'verbs:replay {--force} {--reattach}'; protected $description = 'Replay all Verbs events.'; - public function handle(BrokersEvents $broker): int + public function handle(BrokersEvents $broker, StoresEvents $event_store): int { if (! $this->confirmed() || ! $this->confirmedAgainIfProduction()) { return 1; @@ -39,12 +40,18 @@ public function handle(BrokersEvents $broker): int $progress->start(); $broker->replay( - beforeEach: fn (Event $event) => $progress->label(sprintf( - '[%s] %s::%d', - date('i:s', time() - $started_at), - class_basename($event), - $event->id, - )), + beforeEach: function (Event $event) use ($event_store, $progress, $started_at) { + if ($this->option('reattach')) { + $event_store->reattach([$event]); + } + + $progress->label(sprintf( + '[%s] %s::%d', + date('i:s', time() - $started_at), + class_basename($event), + $event->id, + )); + }, afterEach: fn () => $progress->advance(), ); diff --git a/src/Contracts/StoresEvents.php b/src/Contracts/StoresEvents.php index aa145e18..f3ca20b9 100644 --- a/src/Contracts/StoresEvents.php +++ b/src/Contracts/StoresEvents.php @@ -19,4 +19,7 @@ public function read( /** @param Event[] $events */ public function write(array $events): bool; + + /** @param Event[] $events */ + public function reattach(array $events): bool; } diff --git a/src/Lifecycle/EventStore.php b/src/Lifecycle/EventStore.php index b68e5c1d..0949ab18 100644 --- a/src/Lifecycle/EventStore.php +++ b/src/Lifecycle/EventStore.php @@ -47,6 +47,15 @@ public function write(array $events): bool && VerbStateEvent::insert($this->formatRelationshipsForWrite($events)); } + public function reattach(array $events): bool + { + if (empty($events)) { + return true; + } + + return VerbStateEvent::insertOrIgnore($this->formatRelationshipsForWrite($events)); + } + protected function readEvents( ?State $state, Bits|UuidInterface|AbstractUid|int|string|null $after_id, diff --git a/src/Testing/EventStoreFake.php b/src/Testing/EventStoreFake.php index 508ac711..f024f48d 100644 --- a/src/Testing/EventStoreFake.php +++ b/src/Testing/EventStoreFake.php @@ -57,6 +57,11 @@ public function write(array $events): bool return true; } + public function reattach(array $events): bool + { + return true; + } + /** @return Collection */ public function committed(string $class_name, ?Closure $filter = null): Collection { diff --git a/tests/Feature/ReplayCommandTest.php b/tests/Feature/ReplayCommandTest.php index feaf3b27..73591d95 100644 --- a/tests/Feature/ReplayCommandTest.php +++ b/tests/Feature/ReplayCommandTest.php @@ -9,6 +9,7 @@ use Thunk\Verbs\Facades\Verbs; use Thunk\Verbs\Lifecycle\StateManager; use Thunk\Verbs\Models\VerbSnapshot; +use Thunk\Verbs\Models\VerbStateEvent; use Thunk\Verbs\State; beforeEach(function () { @@ -136,6 +137,41 @@ expect($snapshot2->created_at)->toEqual(CarbonImmutable::parse('2024-05-15 18:00:00')); }); +it('can reattach events to states when replaying', function () { + Carbon::setTestNow('2024-04-01 12:00:00'); + + $state1_id = Id::make(); + $state2_id = Id::make(); + + $state1_event1 = ReplayCommandTestEvent::fire(state_id: $state1_id); + $state2_event1 = ReplayCommandTestEvent::fire(state_id: $state2_id); + $state1_event2 = ReplayCommandTestEvent::fire(state_id: $state1_id); + $state2_event2 = ReplayCommandTestEvent::fire(state_id: $state2_id); + + Verbs::commit(); + + expect(VerbStateEvent::count())->toBe(4); + + $this->artisan(ReplayCommand::class); + + expect(VerbStateEvent::count())->toBe(4); + + VerbStateEvent::truncate(); + + expect(VerbStateEvent::count())->toBe(0); + + $this->artisan(ReplayCommand::class); + + expect(VerbStateEvent::count())->toBe(0); + + $this->artisan(ReplayCommand::class, ['--reattach' => true]); + + expect(VerbStateEvent::where(['state_id' => $state1_id, 'event_id' => $state1_event1->id])->count())->toBe(1) + ->and(VerbStateEvent::where(['state_id' => $state1_id, 'event_id' => $state1_event2->id])->count())->toBe(1) + ->and(VerbStateEvent::where(['state_id' => $state2_id, 'event_id' => $state2_event1->id])->count())->toBe(1) + ->and(VerbStateEvent::where(['state_id' => $state2_id, 'event_id' => $state2_event2->id])->count())->toBe(1); +}); + class ReplayCommandTestEvent extends Event { public function __construct(