Repository: commanded/commanded
Pull request #389 (2020-07-05 10:52:55)

Allow metadata in upcaster
We allowing the Upcaster protocol casses to RecordedEvent metadata regarding the event.
This will allow to solve idepotency issues on EventHandlers and ProcessManagers by allowing to do checks based `event_uuid` or `event_number` as well as having access to `created_at` fields and correlation/causation, stream info.

This PR addresses #339 directly but also indirectly solves #327


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7ac3cc6..3eb28dc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,7 @@
 ### Enhancements
 
 - Capture exception on Process Manager `apply/2` and call `error/3` callback functions ([#380](https://github.com/commanded/commanded/pull/380)).
+- Include metadata in upcaster protocol ([#389](https://github.com/commanded/commanded/pull/389)).
 
 ## v1.1.0
 
diff --git a/lib/commanded/event/upcast.ex b/lib/commanded/event/upcast.ex
index be45ddc..77c87bc 100644
--- a/lib/commanded/event/upcast.ex
+++ b/lib/commanded/event/upcast.ex
@@ -12,9 +12,25 @@ defmodule Commanded.Event.Upcast do
     Enum.map(event_stream, &upcast_event/1)
   end
 
+  @enrich_metadata_fields [
+    :event_id,
+    :event_number,
+    :stream_id,
+    :stream_version,
+    :correlation_id,
+    :causation_id,
+    :created_at
+  ]
+
   defp upcast_event(%RecordedEvent{} = event) do
     %RecordedEvent{data: data, metadata: metadata} = event
 
-    %RecordedEvent{event | data: Upcaster.upcast(data, metadata)}
+    enriched_metadata =
+      event
+      |> Map.from_struct()
+      |> Map.take(@enrich_metadata_fields)
+      |> Map.merge(metadata || %{})
+
+    %RecordedEvent{event | data: Upcaster.upcast(data, enriched_metadata)}
   end
 end


Test patch:
diff --git a/test/event/support/upcast/events.ex b/test/event/support/upcast/events.ex
index e087b30..9e47a61 100644
--- a/test/event/support/upcast/events.ex
+++ b/test/event/support/upcast/events.ex
@@ -27,6 +27,17 @@ defmodule Commanded.Event.Upcast.Events do
     defstruct [:version, :name, :reply_to, :process_id]
   end
 
+  defmodule EventFive do
+    @derive Jason.Encoder
+    defstruct [:version, :reply_to, :process_id, :event_metadata]
+
+    defimpl Upcaster do
+      def upcast(%EventFive{} = event, metadata) do
+        %EventFive{event | version: 2, event_metadata: metadata}
+      end
+    end
+  end
+
   defimpl Upcaster, for: EventThree do
     def upcast(%EventThree{} = event, _metadata) do
       data = Map.from_struct(event) |> Map.put(:name, "Chris") |> Map.put(:version, 2)
diff --git a/test/event/upcaster_test.exs b/test/event/upcaster_test.exs
index efe757f..ee15f21 100644
--- a/test/event/upcaster_test.exs
+++ b/test/event/upcaster_test.exs
@@ -6,7 +6,7 @@ defmodule Event.UpcasterTest do
   alias Commanded.EventStore
   alias Commanded.EventStore.EventData
   alias Commanded.EventStore.RecordedEvent
-  alias Commanded.Event.Upcast.Events.{EventOne, EventTwo, EventThree, EventFour, Stop}
+  alias Commanded.Event.Upcast.Events.{EventOne, EventTwo, EventThree, EventFour, EventFive, Stop}
   alias Commanded.Event.Upcast.UpcastAggregate
 
   setup do
@@ -30,6 +30,27 @@ defmodule Event.UpcasterTest do
       assert version == 2
     end
 
+    test "upcasted event can include event metadata" do
+      assert %EventFive{event_metadata: metadata} =
+               write_events(DefaultApp, struct(EventFive, version: 1)) |> read_event()
+
+      metadata_keys_not_included =
+        [
+          :event_id,
+          :event_number,
+          :stream_id,
+          :stream_version,
+          :correlation_id,
+          :causation_id,
+          :created_at
+        ] -- Map.keys(metadata)
+
+      assert metadata_keys_not_included == []
+      assert is_number(metadata.stream_version)
+      assert is_binary(metadata.event_id)
+      assert %DateTime{} = metadata.created_at
+    end
+
     test "can adapt new event from old event" do
       assert %EventFour{name: name, version: version} =
                write_events(DefaultApp, struct(EventThree, version: 1)) |> read_event()