Class: Lyra::Projections::ModelProjection
- Inherits:
-
Object
- Object
- Lyra::Projections::ModelProjection
- Defined in:
- lib/lyra/projections/model_projection.rb
Overview
Synchronous projection handler for updating model tables from events.
In event_sourcing mode, this class is responsible for keeping the model (read model) tables in sync with the event store. It uses raw SQL operations (insert, update_all, delete_all) to avoid triggering ActiveRecord callbacks or PaperTrail versioning.
Usage:
ModelProjection.project(Registration, :create, result)
ModelProjection.project(Registration, :update, result)
ModelProjection.project(Registration, :destroy, result)
Class Method Summary collapse
-
.project(model_class, operation, result) ⇒ Object
Project a command result to the model table.
-
.project_create(model_class, result) ⇒ Object
Insert a new record into the model table.
-
.project_destroy(model_class, result) ⇒ Object
Delete a record from the model table.
-
.project_update(model_class, result) ⇒ Object
Update an existing record in the model table.
Class Method Details
.project(model_class, operation, result) ⇒ Object
Project a command result to the model table
24 25 26 27 28 29 30 31 32 33 34 35 |
# File 'lib/lyra/projections/model_projection.rb', line 24 def project(model_class, operation, result) case operation when :create project_create(model_class, result) when :update project_update(model_class, result) when :destroy project_destroy(model_class, result) else raise ArgumentError, "Unknown operation: #{operation}" end end |
.project_create(model_class, result) ⇒ Object
Insert a new record into the model table
Uses insert() to bypass all callbacks and validations. This is intentional - the event is the source of truth.
41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
# File 'lib/lyra/projections/model_projection.rb', line 41 def project_create(model_class, result) attributes = result.attributes.dup # Ensure we have an ID raise "Cannot project create without ID" unless attributes[:id] || attributes["id"] # Add timestamps if not present now = Time.current attributes[:created_at] ||= now attributes[:updated_at] ||= now # Convert to database-safe format insert_attrs = sanitize_attributes(model_class, attributes) # Use insert to bypass callbacks # Bypass strict data access - projections are legitimate bulk operations previous = Thread.current[:lyra_bypass_strict_access] Thread.current[:lyra_bypass_strict_access] = true begin model_class.insert(insert_attrs) ensure Thread.current[:lyra_bypass_strict_access] = previous end end |
.project_destroy(model_class, result) ⇒ Object
Delete a record from the model table
Uses delete_all() to bypass callbacks and dependent destroy.
103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 |
# File 'lib/lyra/projections/model_projection.rb', line 103 def project_destroy(model_class, result) event = result.events&.first return unless event model_id = event.data[:model_id] || event.data["model_id"] # Use delete_all to bypass callbacks # Bypass strict data access - projections are legitimate bulk operations previous = Thread.current[:lyra_bypass_strict_access] Thread.current[:lyra_bypass_strict_access] = true begin model_class.where(id: model_id).delete_all ensure Thread.current[:lyra_bypass_strict_access] = previous end end |
.project_update(model_class, result) ⇒ Object
Update an existing record in the model table
Uses update_all() to bypass all callbacks.
69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 |
# File 'lib/lyra/projections/model_projection.rb', line 69 def project_update(model_class, result) event = result.events&.first return unless event model_id = event.data[:model_id] || event.data["model_id"] changes = event.data[:changes] || event.data["changes"] || {} return if changes.empty? # Extract new values from changes (changes are [old_value, new_value] tuples) # Use symbol keys consistently to avoid duplicate column errors updates = {} changes.each do |field, change| new_value = change.is_a?(Array) ? change.last : change updates[field.to_sym] = new_value end # Ensure updated_at timestamp (may already be in changes) updates[:updated_at] ||= Time.current # Use update_all to bypass callbacks # Bypass strict data access - projections are legitimate bulk operations previous = Thread.current[:lyra_bypass_strict_access] Thread.current[:lyra_bypass_strict_access] = true begin model_class.where(id: model_id).update_all(updates) ensure Thread.current[:lyra_bypass_strict_access] = previous end end |