funes-rails 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +13 -3
- data/app/event_streams/funes/event_stream.rb +139 -26
- data/app/jobs/funes/persist_projection_job.rb +2 -0
- data/app/projections/funes/projection.rb +49 -10
- data/lib/funes/invalid_materialization_state.rb +31 -0
- data/lib/funes/version.rb +1 -1
- data/lib/funes.rb +1 -0
- metadata +12 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fa21a99408ee10d9f9b17e06af87f2f51d099fd593fadec8672c71b15110a53e
|
|
4
|
+
data.tar.gz: 893a107d52489ffc4e7680f558dbe8dfce0c8bdf88ad05bd4a704efd0eeb97ce
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: eddd2d0d5c46fe75338a095098a5391eb3aa81b29196f4fd352458ba6e6efc75673959ac59b1b40c555ec1a2f8914524503f718017845f636b030577578dc3f6
|
|
7
|
+
data.tar.gz: c57f6bb79093b02c19abc8c5246bb741c4cf23d16c24dc2e1857e7941457e02ddd4a2e83441b767302bf6c4322bb88a087703bf2480031ac0bc7aa5e86470607
|
data/README.md
CHANGED
|
@@ -103,12 +103,22 @@ Funes gives you fine-grained control over when and how projections run:
|
|
|
103
103
|
|
|
104
104
|
Guides and full API documentation are available at [docs.funes.org](https://docs.funes.org).
|
|
105
105
|
|
|
106
|
+
## Performance
|
|
107
|
+
|
|
108
|
+
Precise benchmarks are notoriously hard to pin down: workloads vary, and absolute numbers depend heavily on hardware, configuration, and the shape of the data. So treat the figures we publish as directional rather than definitive.
|
|
109
|
+
|
|
110
|
+
That said, our measurements consistently show that the complexity of event stream operations stays **sub-linear, comfortably within `O(n)`** as the log grows — which is an important property for medium/long-lived streams.
|
|
111
|
+
|
|
112
|
+
You can browse the latest measurements and join the conversation in the [Performance Measurements](https://github.com/funes-org/funes/discussions/categories/performance-measurements) discussions.
|
|
113
|
+
|
|
106
114
|
## Compatibility
|
|
107
115
|
|
|
108
|
-
|
|
109
|
-
|
|
116
|
+
Funes supports the following runtimes:
|
|
117
|
+
|
|
118
|
+
- **Ruby** 3.1 or newer
|
|
119
|
+
- **Rails** 7.2 or newer
|
|
110
120
|
|
|
111
|
-
Rails 8.0
|
|
121
|
+
If you're on Rails 8.0 or above, you'll need Ruby 3.2 or newer — that's a Rails 8 requirement, not a Funes one.
|
|
112
122
|
|
|
113
123
|
## License
|
|
114
124
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
@@ -230,33 +230,72 @@ module Funes
|
|
|
230
230
|
# # Race condition detected, retry logic here
|
|
231
231
|
# end
|
|
232
232
|
def append(new_event, at: nil)
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
occurred_at = resolve_proper_occurred_at(new_event, at)
|
|
236
|
-
|
|
237
|
-
if consistency_projection.present?
|
|
238
|
-
materialization = compute_projection_with_new_event(consistency_projection, new_event, occurred_at)
|
|
239
|
-
transfer_interpretation_errors(new_event)
|
|
240
|
-
return new_event if materialization.invalid? || new_event.invalid?
|
|
241
|
-
end
|
|
242
|
-
|
|
243
|
-
ActiveRecord::Base.transaction do
|
|
244
|
-
begin
|
|
245
|
-
@instance_new_events << new_event.persist!(@idx, incremented_version, at: occurred_at)
|
|
246
|
-
run_transactional_projections
|
|
247
|
-
rescue ActiveRecord::RecordNotUnique
|
|
248
|
-
new_event._event_entry = nil
|
|
249
|
-
new_event.errors.add(:base, I18n.t("funes.events.racing_condition_on_insert"))
|
|
250
|
-
raise ActiveRecord::Rollback
|
|
251
|
-
rescue ActiveRecord::StatementInvalid, ActiveRecord::RecordInvalid => e
|
|
252
|
-
new_event._event_entry = nil
|
|
253
|
-
raise e
|
|
254
|
-
end
|
|
255
|
-
end
|
|
256
|
-
|
|
257
|
-
schedule_async_projections unless new_event.errors.any?
|
|
233
|
+
do_append(new_event, at: at, raise_on_failure: false)
|
|
234
|
+
end
|
|
258
235
|
|
|
259
|
-
|
|
236
|
+
# Append a new event to the stream, raising on any failure.
|
|
237
|
+
#
|
|
238
|
+
# Behaves like {#append}, but raises `ActiveRecord::RecordInvalid` whenever the event cannot be
|
|
239
|
+
# persisted — mirroring the relationship between `ActiveRecord::Base#save` and `#save!`. Because
|
|
240
|
+
# a real exception is raised, any enclosing `ActiveRecord::Base.transaction` block rolls back,
|
|
241
|
+
# which is what you want when you need host-managed transactional control around `append`.
|
|
242
|
+
#
|
|
243
|
+
# The failed event is always queryable after the rescue: `event.persisted?` returns false and
|
|
244
|
+
# `event.errors` is populated (for event-level failures such as validation, consistency
|
|
245
|
+
# rejection, or version conflict).
|
|
246
|
+
#
|
|
247
|
+
# Failure modes:
|
|
248
|
+
# - Event's own validation fails → raises `ActiveRecord::RecordInvalid` with the event as `record`.
|
|
249
|
+
# - Consistency projection rejects the event → raises `ActiveRecord::RecordInvalid` with the
|
|
250
|
+
# event as `record` (state / interpretation errors transferred to the event).
|
|
251
|
+
# - Version conflict (race condition on insert) → raises `ActiveRecord::RecordInvalid` with the
|
|
252
|
+
# event as `record` and a racing-condition message on `event.errors[:base]`.
|
|
253
|
+
# - A transactional projection's persistence fails → the original
|
|
254
|
+
# `ActiveRecord::StatementInvalid` / `ActiveRecord::RecordInvalid` is re-raised untouched (its
|
|
255
|
+
# `record` is the projection's materialization model, not the event). `event.persisted?` is
|
|
256
|
+
# still false after the rescue.
|
|
257
|
+
#
|
|
258
|
+
# Async projections are only enqueued when `append!` returns successfully. When `append!` is
|
|
259
|
+
# nested inside a user-opened `ActiveRecord::Base.transaction` that later rolls back, Rails'
|
|
260
|
+
# `enqueue_after_transaction_commit` discards the deferred enqueue so no job runs.
|
|
261
|
+
#
|
|
262
|
+
# @param [Funes::Event] new_event The event to append to the stream.
|
|
263
|
+
# @param [Time, Date, nil] at See {#append}.
|
|
264
|
+
# @return [Funes::Event] The persisted event.
|
|
265
|
+
# @raise [ActiveRecord::RecordInvalid] When the event cannot be persisted. `e.record` is the
|
|
266
|
+
# failed event (for event-level failures) or a projection materialization model (for
|
|
267
|
+
# projection validation failures).
|
|
268
|
+
# @raise [ActiveRecord::StatementInvalid] When a transactional projection fails with a database
|
|
269
|
+
# constraint violation.
|
|
270
|
+
# @raise [Funes::ConflictingActualTimeError] See {#append}.
|
|
271
|
+
# @raise [Funes::MissingActualTimeAttributeError] See {#append}.
|
|
272
|
+
#
|
|
273
|
+
# @example Host-managed transaction with a sibling AR update
|
|
274
|
+
# event = SomeEvent.new(some: "value")
|
|
275
|
+
# begin
|
|
276
|
+
# ActiveRecord::Base.transaction do
|
|
277
|
+
# some_model.update!(some: "value")
|
|
278
|
+
# SomeEventStream.for(stream_id).append!(event)
|
|
279
|
+
# end
|
|
280
|
+
# rescue ActiveRecord::RecordInvalid
|
|
281
|
+
# event.persisted? # => false
|
|
282
|
+
# event.errors.any? # => true
|
|
283
|
+
# end
|
|
284
|
+
#
|
|
285
|
+
# @example Two appends in a single transaction — either both commit or neither does
|
|
286
|
+
# event_1 = SomeEvent.new(some: "value")
|
|
287
|
+
# event_2 = OtherEvent.new(some: "value")
|
|
288
|
+
# begin
|
|
289
|
+
# ActiveRecord::Base.transaction do
|
|
290
|
+
# SomeEventStream.for(stream_id).append!(event_1)
|
|
291
|
+
# OtherEventStream.for(other_stream_id).append!(event_2)
|
|
292
|
+
# end
|
|
293
|
+
# rescue ActiveRecord::RecordInvalid
|
|
294
|
+
# event_1.persisted? # => false
|
|
295
|
+
# event_2.persisted? # => false
|
|
296
|
+
# end
|
|
297
|
+
def append!(new_event, at: nil)
|
|
298
|
+
do_append(new_event, at: at, raise_on_failure: true)
|
|
260
299
|
end
|
|
261
300
|
|
|
262
301
|
# @!visibility private
|
|
@@ -291,12 +330,19 @@ module Funes
|
|
|
291
330
|
# include those where `occurred_at <= at` before projection. When `as_of:` is provided,
|
|
292
331
|
# it overrides the stream's record-time boundary, filtering events in Ruby by `created_at`.
|
|
293
332
|
#
|
|
333
|
+
# Mirrors `ActiveRecord::Base.find`: if there are no events to replay — either because the
|
|
334
|
+
# stream has no events at all or because `as_of:` / `at:` filtering leaves none in scope —
|
|
335
|
+
# raises {ActiveRecord::RecordNotFound}. In a Rails controller, this lets the host app's
|
|
336
|
+
# middleware render a 404 automatically, without any rescue wiring.
|
|
337
|
+
#
|
|
294
338
|
# @param projection_class [Class<Funes::Projection>] The projection class to use.
|
|
295
339
|
# @param [Time, nil] as_of Optional record-time override. When provided, only events with
|
|
296
340
|
# `created_at <= as_of` are considered, overriding the stream's own `@as_of`.
|
|
297
341
|
# @param [Time, nil] at Optional actual-time reference. When provided, only events with
|
|
298
342
|
# `occurred_at <= at` are included in the projection.
|
|
299
343
|
# @return [Object] The materialized state as defined by the projection's materialization model.
|
|
344
|
+
# @raise [ActiveRecord::RecordNotFound] when the filtered event list is empty — either
|
|
345
|
+
# because the stream has no events or because `as_of:` / `at:` excludes them all.
|
|
300
346
|
#
|
|
301
347
|
# @example Project current state
|
|
302
348
|
# stream = OrderEventStream.for("order-123")
|
|
@@ -313,9 +359,36 @@ module Funes
|
|
|
313
359
|
# snapshot = stream.projected_with(SalaryProjection,
|
|
314
360
|
# as_of: Time.new(2025, 3, 1),
|
|
315
361
|
# at: Time.new(2025, 2, 20))
|
|
362
|
+
#
|
|
363
|
+
# @example Automatic 404 in a Rails controller
|
|
364
|
+
# class OrdersController < ApplicationController
|
|
365
|
+
# def show
|
|
366
|
+
# stream = OrderEventStream.for(params[:id])
|
|
367
|
+
# @order = stream.projected_with(OrderSummaryProjection)
|
|
368
|
+
# end
|
|
369
|
+
# end
|
|
370
|
+
# # If no events exist for params[:id], Rails renders its 404 page automatically.
|
|
316
371
|
def projected_with(projection_class, as_of: nil, at: nil)
|
|
317
372
|
source_events = as_of ? filter_by_record_time(events, as_of) : events
|
|
318
373
|
target_events = at ? filter_by_actual_time(source_events, at) : source_events
|
|
374
|
+
|
|
375
|
+
if target_events.empty?
|
|
376
|
+
materialization_model = projection_class.instance_variable_get(:@materialization_model)
|
|
377
|
+
model_name = materialization_model&.name
|
|
378
|
+
Rails.logger.info(
|
|
379
|
+
"[Funes] projected_with found no events for " \
|
|
380
|
+
"#{self.class.name} idx=#{idx.inspect} " \
|
|
381
|
+
"projection=#{projection_class.name} " \
|
|
382
|
+
"as_of=#{as_of.inspect} at=#{at.inspect}"
|
|
383
|
+
)
|
|
384
|
+
raise ActiveRecord::RecordNotFound.new(
|
|
385
|
+
"Couldn't find #{model_name} for #{self.class.name} #{idx.inspect}",
|
|
386
|
+
model_name,
|
|
387
|
+
"idx",
|
|
388
|
+
idx
|
|
389
|
+
)
|
|
390
|
+
end
|
|
391
|
+
|
|
319
392
|
projection_class.process_events(target_events, at: at)
|
|
320
393
|
end
|
|
321
394
|
|
|
@@ -338,6 +411,46 @@ module Funes
|
|
|
338
411
|
end
|
|
339
412
|
|
|
340
413
|
private
|
|
414
|
+
def do_append(new_event, at:, raise_on_failure:)
|
|
415
|
+
unless new_event.valid?
|
|
416
|
+
raise ActiveRecord::RecordInvalid.new(new_event) if raise_on_failure
|
|
417
|
+
return new_event
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
occurred_at = resolve_proper_occurred_at(new_event, at)
|
|
421
|
+
|
|
422
|
+
if consistency_projection.present?
|
|
423
|
+
materialization = compute_projection_with_new_event(consistency_projection, new_event, occurred_at)
|
|
424
|
+
transfer_interpretation_errors(new_event)
|
|
425
|
+
if materialization.invalid? || new_event.invalid?
|
|
426
|
+
raise ActiveRecord::RecordInvalid.new(new_event) if raise_on_failure
|
|
427
|
+
return new_event
|
|
428
|
+
end
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
ActiveRecord::Base.transaction do
|
|
432
|
+
begin
|
|
433
|
+
@instance_new_events << new_event.persist!(@idx, incremented_version, at: occurred_at)
|
|
434
|
+
ActiveRecord::Base.connection.current_transaction.after_rollback do
|
|
435
|
+
new_event._event_entry = nil
|
|
436
|
+
end
|
|
437
|
+
run_transactional_projections
|
|
438
|
+
rescue ActiveRecord::RecordNotUnique
|
|
439
|
+
new_event._event_entry = nil
|
|
440
|
+
new_event.errors.add(:base, I18n.t("funes.events.racing_condition_on_insert"))
|
|
441
|
+
raise ActiveRecord::RecordInvalid.new(new_event) if raise_on_failure
|
|
442
|
+
raise ActiveRecord::Rollback
|
|
443
|
+
rescue ActiveRecord::StatementInvalid, ActiveRecord::RecordInvalid => e
|
|
444
|
+
new_event._event_entry = nil
|
|
445
|
+
raise e
|
|
446
|
+
end
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
schedule_async_projections unless new_event.errors.any?
|
|
450
|
+
|
|
451
|
+
new_event
|
|
452
|
+
end
|
|
453
|
+
|
|
341
454
|
def run_transactional_projections
|
|
342
455
|
transactional_projections.each do |projection_class|
|
|
343
456
|
Funes::PersistProjectionJob.perform_now(self, projection_class, last_event_creation_date,
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
module Funes
|
|
2
2
|
class PersistProjectionJob < ApplicationJob
|
|
3
|
+
self.enqueue_after_transaction_commit = true
|
|
4
|
+
|
|
3
5
|
def perform(event_stream, projection_class, as_of = nil, at = nil)
|
|
4
6
|
events = as_of ? event_stream.events.select { |e| e.created_at <= as_of } : event_stream.events
|
|
5
7
|
projection_class.materialize!(events, event_stream.idx, at: at)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
require "funes/unknown_event"
|
|
2
2
|
require "funes/unknown_materialization_model"
|
|
3
|
+
require "funes/invalid_materialization_state"
|
|
3
4
|
|
|
4
5
|
module Funes
|
|
5
6
|
# Projections perform the necessary pattern-matching to compute and aggregate the interpretations that the system
|
|
@@ -122,6 +123,31 @@ module Funes
|
|
|
122
123
|
@materialization_model = active_record_or_model
|
|
123
124
|
end
|
|
124
125
|
|
|
126
|
+
# Registers an instance method on the materialization model that owns the persistence step,
|
|
127
|
+
# replacing the framework's default upsert.
|
|
128
|
+
#
|
|
129
|
+
# When set, the named method is invoked on the in-memory state after all interpretations have
|
|
130
|
+
# run and +idx+ has been assigned. The method takes no arguments and is expected to raise on
|
|
131
|
+
# failure. The framework still calls +state.valid?+ and raises
|
|
132
|
+
# +Funes::InvalidMaterializationState+ before delegating, so the custom method only runs
|
|
133
|
+
# against valid state.
|
|
134
|
+
#
|
|
135
|
+
# Declaring +persist_materialization_model_with+ also lifts the requirement that the
|
|
136
|
+
# materialization model be an +ActiveRecord::Base+ subclass: a plain +ActiveModel+ class is
|
|
137
|
+
# enough as long as it exposes +assign_attributes+, +attributes+, +valid?+ and +errors+.
|
|
138
|
+
#
|
|
139
|
+
# @param [Symbol] method_name The instance method on the materialization model that performs the write.
|
|
140
|
+
# @return [void]
|
|
141
|
+
#
|
|
142
|
+
# @example Persisting a projection to a JSON file
|
|
143
|
+
# class YourProjection < Funes::Projection
|
|
144
|
+
# materialization_model YourMaterializationModel
|
|
145
|
+
# persist_materialization_model_with :save_to_object_storage!
|
|
146
|
+
# end
|
|
147
|
+
def persist_materialization_model_with(method_name)
|
|
148
|
+
@persist_method = method_name
|
|
149
|
+
end
|
|
150
|
+
|
|
125
151
|
# It changes the sensibility of the projection about events that it doesn't know how to interpret
|
|
126
152
|
#
|
|
127
153
|
# By default, a projection ignores events that it doesn't have interpretations for. This method informs the
|
|
@@ -142,7 +168,8 @@ module Funes
|
|
|
142
168
|
def process_events(events_collection, at: nil, consistency: false)
|
|
143
169
|
new(self.instance_variable_get(:@interpretations),
|
|
144
170
|
self.instance_variable_get(:@materialization_model),
|
|
145
|
-
self.instance_variable_get(:@throws_on_unknown_events)
|
|
171
|
+
self.instance_variable_get(:@throws_on_unknown_events),
|
|
172
|
+
self.instance_variable_get(:@persist_method))
|
|
146
173
|
.process_events(events_collection, at: at, consistency: consistency)
|
|
147
174
|
end
|
|
148
175
|
|
|
@@ -150,13 +177,14 @@ module Funes
|
|
|
150
177
|
def materialize!(events_collection, idx, at: nil)
|
|
151
178
|
new(self.instance_variable_get(:@interpretations),
|
|
152
179
|
self.instance_variable_get(:@materialization_model),
|
|
153
|
-
self.instance_variable_get(:@throws_on_unknown_events)
|
|
180
|
+
self.instance_variable_get(:@throws_on_unknown_events),
|
|
181
|
+
self.instance_variable_get(:@persist_method))
|
|
154
182
|
.materialize!(events_collection, idx, at: at)
|
|
155
183
|
end
|
|
156
184
|
end
|
|
157
185
|
|
|
158
186
|
# @!visibility private
|
|
159
|
-
def initialize(interpretations, materialization_model, throws_on_unknown_events)
|
|
187
|
+
def initialize(interpretations, materialization_model, throws_on_unknown_events, persist_method = nil)
|
|
160
188
|
@interpretations = interpretations
|
|
161
189
|
@materialization_model = materialization_model
|
|
162
190
|
raise Funes::UnknownMaterializationModel,
|
|
@@ -164,6 +192,7 @@ module Funes
|
|
|
164
192
|
|
|
165
193
|
|
|
166
194
|
@throws_on_unknown_events = throws_on_unknown_events
|
|
195
|
+
@persist_method = persist_method
|
|
167
196
|
end
|
|
168
197
|
|
|
169
198
|
# @!visibility private
|
|
@@ -191,13 +220,13 @@ module Funes
|
|
|
191
220
|
# @!visibility private
|
|
192
221
|
def materialize!(events_collection, idx, at: nil)
|
|
193
222
|
state = process_events(events_collection, at: at)
|
|
194
|
-
|
|
195
|
-
if materialization_model_is_persistable?
|
|
223
|
+
if persistable?
|
|
196
224
|
state.assign_attributes(idx:)
|
|
197
225
|
persist_based_on!(state)
|
|
226
|
+
return state if @persist_method
|
|
198
227
|
end
|
|
199
228
|
|
|
200
|
-
|
|
229
|
+
materialized_model_instance_based_on(state)
|
|
201
230
|
end
|
|
202
231
|
|
|
203
232
|
private
|
|
@@ -213,13 +242,23 @@ module Funes
|
|
|
213
242
|
@materialization_model.new(state.attributes)
|
|
214
243
|
end
|
|
215
244
|
|
|
216
|
-
def
|
|
217
|
-
@materialization_model.present?
|
|
245
|
+
def persistable?
|
|
246
|
+
return false unless @materialization_model.present?
|
|
247
|
+
return true if @persist_method
|
|
248
|
+
@materialization_model <= ActiveRecord::Base
|
|
218
249
|
end
|
|
219
250
|
|
|
220
251
|
def persist_based_on!(state)
|
|
221
|
-
|
|
222
|
-
|
|
252
|
+
unless state.valid?
|
|
253
|
+
raise Funes::InvalidMaterializationState.new(state) if @persist_method
|
|
254
|
+
raise ActiveRecord::RecordInvalid.new(state)
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
if @persist_method
|
|
258
|
+
state.public_send(@persist_method)
|
|
259
|
+
else
|
|
260
|
+
@materialization_model.upsert(state.attributes, unique_by: :idx)
|
|
261
|
+
end
|
|
223
262
|
end
|
|
224
263
|
|
|
225
264
|
def warn_about_ineffective_errors(event)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module Funes
|
|
2
|
+
# Raised when a projection configured with `persist_materialization_model_with` produces an
|
|
3
|
+
# invalid materialization state.
|
|
4
|
+
#
|
|
5
|
+
# When a projection declares a custom persistence method via `persist_materialization_model_with`,
|
|
6
|
+
# Funes still validates the materialization (via `state.valid?`) before delegating the write.
|
|
7
|
+
# If validation fails, this exception is raised and the custom persist method is *not* invoked.
|
|
8
|
+
#
|
|
9
|
+
# The failed materialization is exposed via `#record`, mirroring the ergonomics of
|
|
10
|
+
# `ActiveRecord::RecordInvalid` — but without coupling the custom-persist path to ActiveRecord.
|
|
11
|
+
# Like any unrescued exception, it propagates out of `materialize!` and rolls back any enclosing
|
|
12
|
+
# `ActiveRecord::Base.transaction`, which is the desired behaviour for transactional projections.
|
|
13
|
+
#
|
|
14
|
+
# @example Handling an invalid materialization state
|
|
15
|
+
# begin
|
|
16
|
+
# YourProjection.materialize!(events, "some-id", at: Time.current)
|
|
17
|
+
# rescue Funes::InvalidMaterializationState => e
|
|
18
|
+
# Rails.logger.error "Invalid materialization: #{e.message}"
|
|
19
|
+
# e.record.errors.full_messages # the validation errors on the materialization
|
|
20
|
+
# end
|
|
21
|
+
#
|
|
22
|
+
# @see Funes::Projection.persist_materialization_model_with
|
|
23
|
+
class InvalidMaterializationState < StandardError
|
|
24
|
+
attr_reader :record
|
|
25
|
+
|
|
26
|
+
def initialize(record)
|
|
27
|
+
@record = record
|
|
28
|
+
super("Invalid materialization state: #{record.errors.full_messages.to_sentence}")
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
data/lib/funes/version.rb
CHANGED
data/lib/funes.rb
CHANGED
|
@@ -5,6 +5,7 @@ require "funes/event_metainformation_builder"
|
|
|
5
5
|
require "funes/invalid_event_metainformation"
|
|
6
6
|
require "funes/conflicting_actual_time_error"
|
|
7
7
|
require "funes/missing_actual_time_attribute_error"
|
|
8
|
+
require "funes/invalid_materialization_state"
|
|
8
9
|
require "funes/engine"
|
|
9
10
|
|
|
10
11
|
# Funes is an event sourcing framework for Ruby on Rails.
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: funes-rails
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Vinícius Almeida da Silva
|
|
@@ -15,28 +15,34 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - ">="
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: '7.
|
|
18
|
+
version: '7.2'
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - ">="
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: '7.
|
|
25
|
+
version: '7.2'
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: minitest
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
29
29
|
requirements:
|
|
30
|
-
- - "
|
|
30
|
+
- - ">="
|
|
31
31
|
- !ruby/object:Gem::Version
|
|
32
32
|
version: '5.26'
|
|
33
|
+
- - "<"
|
|
34
|
+
- !ruby/object:Gem::Version
|
|
35
|
+
version: '6'
|
|
33
36
|
type: :development
|
|
34
37
|
prerelease: false
|
|
35
38
|
version_requirements: !ruby/object:Gem::Requirement
|
|
36
39
|
requirements:
|
|
37
|
-
- - "
|
|
40
|
+
- - ">="
|
|
38
41
|
- !ruby/object:Gem::Version
|
|
39
42
|
version: '5.26'
|
|
43
|
+
- - "<"
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
version: '6'
|
|
40
46
|
- !ruby/object:Gem::Dependency
|
|
41
47
|
name: database_cleaner-active_record
|
|
42
48
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -210,6 +216,7 @@ files:
|
|
|
210
216
|
- lib/funes/event_metainformation_builder.rb
|
|
211
217
|
- lib/funes/inspection.rb
|
|
212
218
|
- lib/funes/invalid_event_metainformation.rb
|
|
219
|
+
- lib/funes/invalid_materialization_state.rb
|
|
213
220
|
- lib/funes/missing_actual_time_attribute_error.rb
|
|
214
221
|
- lib/funes/unknown_event.rb
|
|
215
222
|
- lib/funes/unknown_materialization_model.rb
|