Skip to content

Commit 040177b

Browse files
committed
Added and explained PreserveTypes and importance of transformations order.
1 parent 257a4df commit 040177b

File tree

1 file changed

+79
-3
lines changed

1 file changed

+79
-3
lines changed

posts/2025-09-12-evolving-event-names-and-payloads-in-rails-event-store-without-breaking-history.md

Lines changed: 79 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ We decided to create a custom `Transformations::RefundToReturnEventMapper`and in
5151
```ruby
5252
mapper = RubyEventStore::Mappers::PipelineMapper.new(
5353
RubyEventStore::Mappers::Pipeline.new(
54+
preserve_types, # Explained below
5455
Transformations::RefundToReturnEventMapper.new(
5556
'Ordering::DraftRefundCreated' => 'Ordering::DraftReturnCreated',
5657
'Ordering::ItemAddedToRefund' => 'Ordering::ItemAddedToReturn',
@@ -122,10 +123,10 @@ module Transformations
122123
def transform_payload(data, old_class_name)
123124
case old_class_name
124125
when 'Ordering::DraftRefundCreated'
125-
data = transform_refund_to_return_payload(data, "refund_id", "return_id")
126-
transform_refund_to_return_payload(data, "refundable_products", "returnable_products")
126+
data = transform_refund_to_return_payload(data, :refund_id, :return_id)
127+
transform_refund_to_return_payload(data, :refundable_products, :returnable_products)
127128
when 'Ordering::ItemAddedToRefund', 'Ordering::ItemRemovedFromRefund'
128-
transform_refund_to_return_payload(data, "refund_id", "return_id")
129+
transform_refund_to_return_payload(data, :refund_id, :return_id)
129130
else
130131
data
131132
end
@@ -144,6 +145,81 @@ module Transformations
144145
end
145146
```
146147

148+
#### Preserve types
149+
150+
Preserve types transformation is provided by RES and its job is to restore original types for event data and metadata. When data and metadata are hashes (the most common case), registered types will be restored according to their configuration. This means, in particular, that data keys will be symbolized if they were originally symbols and if config for `Symbol` was registered for `PreserveTypes` transformation.
151+
152+
Let's set up `PreserveTypes` with the same types config that is used by `RailsEventStore::JSONClient` default mapper:
153+
154+
155+
```ruby
156+
preserve_types = begin
157+
preserve_types = RubyEventStore::Mappers::Transformation::PreserveTypes.new
158+
159+
types_config = {
160+
Symbol => {
161+
serializer: ->(v) { v.to_s },
162+
deserializer: ->(v) { v.to_sym }
163+
},
164+
Time => {
165+
serializer: ->(v) { v.iso8601(RubyEventStore::TIMESTAMP_PRECISION) },
166+
deserializer: ->(v) { Time.iso8601(v) }
167+
},
168+
Date => {
169+
serializer: ->(v) { v.iso8601 },
170+
deserializer: ->(v) { Date.iso8601(v) }
171+
},
172+
DateTime => {
173+
serializer: ->(v) { v.iso8601 },
174+
deserializer: ->(v) { DateTime.iso8601(v) }
175+
},
176+
BigDecimal => {
177+
serializer: ->(v) { v.to_s },
178+
deserializer: ->(v) { BigDecimal(v) }
179+
}
180+
}
181+
182+
if defined?(ActiveSupport::TimeWithZone)
183+
types_config[ActiveSupport::TimeWithZone] = {
184+
serializer: ->(v) { v.iso8601(RubyEventStore::TIMESTAMP_PRECISION) },
185+
deserializer: ->(v) { Time.iso8601(v).in_time_zone },
186+
stored_type: ->(*) { "ActiveSupport::TimeWithZone" }
187+
}
188+
end
189+
190+
if defined?(OpenStruct)
191+
types_config[OpenStruct] = {
192+
serializer: ->(v) { v.to_h },
193+
deserializer: ->(v) { OpenStruct.new(v) }
194+
}
195+
end
196+
197+
types_config.each do |type, config|
198+
preserve_types.register(type, **config)
199+
end
200+
201+
preserve_types
202+
end
203+
204+
```
205+
206+
Now we can use it in the transformation pipeline as shown above.
207+
208+
There's one more thing - `PreserveTypes` uses event metadata to store information about what needs to be transformed and how, something like this:
209+
210+
```ruby
211+
"types": {
212+
"data": {
213+
"order_id": ["Symbol", "String"],
214+
"refund_id": ["Symbol", "String"]
215+
}
216+
}
217+
```
218+
219+
this will result in data keys with these names being restored from String to Symbols.
220+
221+
Why does this matter in our case? Because old events were stored with `refund_id` data key, so `PreserveTypes` has to run before our custom `RefundToReturnEventMapper`. Otherwise it won't be able to symbolize `return_id` key because there's no corresponding type data in the event's metadata.
222+
147223
### Other considerations
148224

149225
1. We use load-time transformation only: load() transforms when reading, dump() preserves original format

0 commit comments

Comments
 (0)