nats_wave 1.1.8 → 1.1.10

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.
@@ -1,3 +1,182 @@
1
+ # # frozen_string_literal: true
2
+ #
3
+ # module NatsWave
4
+ # module Concerns
5
+ # module Mappable
6
+ # extend ActiveSupport::Concern
7
+ #
8
+ # included do
9
+ # class_attribute :nats_wave_mapping_config, :nats_wave_subscription_config
10
+ # self.nats_wave_mapping_config = {}
11
+ # self.nats_wave_subscription_config = {}
12
+ # end
13
+ #
14
+ # class_methods do
15
+ # # Configure how this model maps to external models
16
+ # def nats_wave_maps_to(external_models)
17
+ # self.nats_wave_mapping_config = external_models
18
+ #
19
+ # # Register this mapping globally
20
+ # NatsWave::ModelRegistry.register_mapping(self.name, external_models)
21
+ # end
22
+ #
23
+ # # Configure how external models map to this model AND what subjects to subscribe to
24
+ # def nats_wave_maps_from(external_model, options = {})
25
+ # Rails.logger.debug "🔄 #{self.name}: Setting up mapping from #{external_model} with options: #{options.inspect}" if defined?(Rails)
26
+ #
27
+ # mapping = {
28
+ # field_mappings: options[:field_mappings] || {},
29
+ # transformations: options[:transformations] || {},
30
+ # conditions: options[:conditions] || {},
31
+ # sync_strategy: options[:sync_strategy] || :upsert,
32
+ # unique_fields: options[:unique_fields] || [:id],
33
+ # skip_fields: options[:skip_fields] || [],
34
+ # subjects: options[:subjects] || [],
35
+ # handler: options[:handler],
36
+ # queue_group: options[:queue_group]
37
+ # }
38
+ #
39
+ # self.nats_wave_mapping_config[external_model] = mapping
40
+ #
41
+ # Rails.logger.debug "🔄 #{self.name}: Registering reverse mapping for #{external_model}" if defined?(Rails)
42
+ # NatsWave::ModelRegistry.register_reverse_mapping(external_model, self.name, mapping)
43
+ #
44
+ # # Register subscription if subjects are provided
45
+ # if mapping[:subjects].any?
46
+ # Rails.logger.debug "🔄 #{self.name}: Registering subscription for subjects: #{mapping[:subjects]}" if defined?(Rails)
47
+ # NatsWave::ModelRegistry.register_subscription(
48
+ # subjects: mapping[:subjects],
49
+ # model: self.name,
50
+ # external_model: external_model,
51
+ # handler: mapping[:handler],
52
+ # queue_group: mapping[:queue_group]
53
+ # )
54
+ # else
55
+ # Rails.logger.debug "🔄 #{self.name}: No subjects provided, skipping subscription registration" if defined?(Rails)
56
+ # end
57
+ # end
58
+ #
59
+ # # Configure subscriptions without model mapping (for custom handlers)
60
+ # def nats_wave_subscribes_to(*subjects, handler: nil, queue_group: nil, &block)
61
+ # handler ||= block
62
+ #
63
+ # Rails.logger.debug "📡 #{self.name}: Setting up subscription to subjects: #{subjects.inspect}" if defined?(Rails)
64
+ #
65
+ # subscription_config = {
66
+ # subjects: subjects.flatten,
67
+ # handler: handler,
68
+ # queue_group: queue_group,
69
+ # model: self.name
70
+ # }
71
+ #
72
+ # self.nats_wave_subscription_config[:custom] = subscription_config
73
+ #
74
+ # Rails.logger.debug "📡 #{self.name}: Registering subscription in ModelRegistry" if defined?(Rails)
75
+ # NatsWave::ModelRegistry.register_subscription(
76
+ # subjects: subjects.flatten,
77
+ # model: self.name,
78
+ # handler: handler,
79
+ # queue_group: queue_group
80
+ # )
81
+ # end
82
+ #
83
+ # # Auto-generate subjects based on external model pattern
84
+ # def nats_wave_auto_subjects_for(external_model, service_prefix: nil, actions: [:create, :update, :destroy])
85
+ # model_name = external_model.underscore
86
+ #
87
+ # if service_prefix
88
+ # subjects = actions.map { |action| "#{service_prefix}.#{model_name}.#{action}" }
89
+ # subjects << "#{service_prefix}.#{model_name}.*" # Wildcard for all actions
90
+ # else
91
+ # # Try to infer from common patterns
92
+ # subjects = [
93
+ # "#{model_name}.*", # Simple pattern
94
+ # "events.#{model_name}.*", # Events pattern
95
+ # "*.#{model_name}.*", # Service.model pattern
96
+ # "*.events.#{model_name}.*" # Service.events.model pattern
97
+ # ]
98
+ # end
99
+ #
100
+ # subjects
101
+ # end
102
+ #
103
+ # # Get mapping configuration for an external model
104
+ # def nats_wave_mapping_for(external_model)
105
+ # nats_wave_mapping_config[external_model]
106
+ # end
107
+ #
108
+ # # Check if this model can sync from an external model
109
+ # def nats_wave_can_sync_from?(external_model)
110
+ # nats_wave_mapping_config.key?(external_model)
111
+ # end
112
+ #
113
+ # # Get all external models this model can sync from
114
+ # def nats_wave_external_models
115
+ # nats_wave_mapping_config.keys
116
+ # end
117
+ #
118
+ # # Get all subjects this model subscribes to
119
+ # def nats_wave_subscribed_subjects
120
+ # subjects = []
121
+ #
122
+ # # From model mappings
123
+ # nats_wave_mapping_config.each do |_, config|
124
+ # subjects.concat(config[:subjects] || [])
125
+ # end
126
+ #
127
+ # # From custom subscriptions
128
+ # if nats_wave_subscription_config[:custom]
129
+ # subjects.concat(nats_wave_subscription_config[:custom][:subjects] || [])
130
+ # end
131
+ #
132
+ # subjects.uniq
133
+ # end
134
+ # end
135
+ #
136
+ # # Instance methods remain the same...
137
+ # def nats_wave_mapped_attributes_for(target_model)
138
+ # mapping = self.class.nats_wave_mapping_config[target_model]
139
+ # return attributes unless mapping
140
+ #
141
+ # mapped_attrs = {}
142
+ # field_mappings = mapping[:field_mappings] || {}
143
+ # skip_fields = mapping[:skip_fields] || []
144
+ #
145
+ # attributes.each do |key, value|
146
+ # next if skip_fields.include?(key.to_s) || skip_fields.include?(key.to_sym)
147
+ #
148
+ # mapped_key = field_mappings[key] || field_mappings[key.to_sym] || key
149
+ # mapped_attrs[mapped_key] = transform_value(value, mapped_key, mapping[:transformations] || {})
150
+ # end
151
+ #
152
+ # mapped_attrs
153
+ # end
154
+ #
155
+ # def nats_wave_unique_identifier_for(external_model)
156
+ # mapping = self.class.nats_wave_mapping_config[external_model]
157
+ # unique_fields = mapping&.dig(:unique_fields) || [:id]
158
+ #
159
+ # unique_fields.map { |field| [field, send(field)] }.to_h
160
+ # end
161
+ #
162
+ # private
163
+ #
164
+ # def transform_value(value, field, transformations)
165
+ # transformation = transformations[field] || transformations[field.to_sym]
166
+ #
167
+ # case transformation
168
+ # when Proc
169
+ # transformation.call(value)
170
+ # when Symbol
171
+ # send(transformation, value) if respond_to?(transformation, true)
172
+ # else
173
+ # value
174
+ # end
175
+ # end
176
+ # end
177
+ # end
178
+ # end
179
+
1
180
  # frozen_string_literal: true
2
181
 
3
182
  module NatsWave
@@ -6,172 +185,357 @@ module NatsWave
6
185
  extend ActiveSupport::Concern
7
186
 
8
187
  included do
9
- class_attribute :nats_wave_mapping_config, :nats_wave_subscription_config
10
- self.nats_wave_mapping_config = {}
188
+ class_attribute :nats_wave_subscription_config, :nats_wave_publishing_config
11
189
  self.nats_wave_subscription_config = {}
190
+ self.nats_wave_publishing_config = {}
12
191
  end
13
192
 
14
193
  class_methods do
15
- # Configure how this model maps to external models
16
- def nats_wave_maps_to(external_models)
17
- self.nats_wave_mapping_config = external_models
194
+ # Configure subscriptions with optional custom handler
195
+ def nats_wave_subscribes_to(*subjects, **options, &block)
196
+ Rails.logger.debug "📡 #{self.name}: Setting up subscription to subjects: #{subjects.inspect}" if defined?(Rails)
18
197
 
19
- # Register this mapping globally
20
- NatsWave::ModelRegistry.register_mapping(self.name, external_models)
21
- end
22
-
23
- # Configure how external models map to this model AND what subjects to subscribe to
24
- def nats_wave_maps_from(external_model, options = {})
25
- Rails.logger.debug "🔄 #{self.name}: Setting up mapping from #{external_model} with options: #{options.inspect}" if defined?(Rails)
198
+ handler = options[:handler] || block
26
199
 
27
- mapping = {
200
+ subscription_config = {
201
+ subjects: subjects.flatten,
28
202
  field_mappings: options[:field_mappings] || {},
29
203
  transformations: options[:transformations] || {},
30
- conditions: options[:conditions] || {},
31
- sync_strategy: options[:sync_strategy] || :upsert,
32
- unique_fields: options[:unique_fields] || [:id],
33
204
  skip_fields: options[:skip_fields] || [],
34
- subjects: options[:subjects] || [],
35
- handler: options[:handler],
36
- queue_group: options[:queue_group]
205
+ unique_fields: options[:unique_fields] || [:id],
206
+ sync_strategy: options[:sync_strategy] || :upsert,
207
+ handler: handler,
208
+ queue_group: options[:queue_group],
209
+ auto_sync: options[:auto_sync] != false # Default to true, can be disabled
37
210
  }
38
211
 
39
- self.nats_wave_mapping_config[external_model] = mapping
40
-
41
- Rails.logger.debug "🔄 #{self.name}: Registering reverse mapping for #{external_model}" if defined?(Rails)
42
- NatsWave::ModelRegistry.register_reverse_mapping(external_model, self.name, mapping)
43
-
44
- # Register subscription if subjects are provided
45
- if mapping[:subjects].any?
46
- Rails.logger.debug "🔄 #{self.name}: Registering subscription for subjects: #{mapping[:subjects]}" if defined?(Rails)
47
- NatsWave::ModelRegistry.register_subscription(
48
- subjects: mapping[:subjects],
49
- model: self.name,
50
- external_model: external_model,
51
- handler: mapping[:handler],
52
- queue_group: mapping[:queue_group]
53
- )
54
- else
55
- Rails.logger.debug "🔄 #{self.name}: No subjects provided, skipping subscription registration" if defined?(Rails)
56
- end
212
+ # Store the config
213
+ config_key = subjects.join(',')
214
+ self.nats_wave_subscription_config[config_key] = subscription_config
215
+
216
+ # Create the subscription handler
217
+ processed_handler = create_subscription_handler(subscription_config)
218
+
219
+ # Register the subscription
220
+ NatsWave::ModelRegistry.register_subscription(
221
+ subjects: subjects.flatten,
222
+ model: self.name,
223
+ handler: processed_handler,
224
+ queue_group: subscription_config[:queue_group]
225
+ )
226
+
227
+ Rails.logger.debug "📡 #{self.name}: Registered subscription for subjects: #{subjects}" if defined?(Rails)
57
228
  end
58
229
 
59
- # Configure subscriptions without model mapping (for custom handlers)
60
- def nats_wave_subscribes_to(*subjects, handler: nil, queue_group: nil, &block)
61
- handler ||= block
230
+ # Configure publishing with optional custom handler
231
+ def nats_wave_publishes_to(*subjects, **options, &block)
232
+ Rails.logger.debug "📤 #{self.name}: Setting up publishing to subjects: #{subjects.inspect}" if defined?(Rails)
62
233
 
63
- Rails.logger.debug "📡 #{self.name}: Setting up subscription to subjects: #{subjects.inspect}" if defined?(Rails)
234
+ handler = options[:handler] || block
64
235
 
65
- subscription_config = {
236
+ publishing_config = {
66
237
  subjects: subjects.flatten,
238
+ field_mappings: options[:field_mappings] || {},
239
+ transformations: options[:transformations] || {},
240
+ skip_fields: options[:skip_fields] || [],
241
+ conditions: options[:conditions] || {},
242
+ actions: options[:actions] || [:create, :update, :destroy],
67
243
  handler: handler,
68
- queue_group: queue_group,
69
- model: self.name
244
+ async: options[:async] != false, # Default to true
245
+ enabled: options[:enabled] != false # Default to true
70
246
  }
71
247
 
72
- self.nats_wave_subscription_config[:custom] = subscription_config
248
+ # Store the config
249
+ config_key = subjects.join(',')
250
+ self.nats_wave_publishing_config[config_key] = publishing_config
73
251
 
74
- Rails.logger.debug "📡 #{self.name}: Registering subscription in ModelRegistry" if defined?(Rails)
75
- NatsWave::ModelRegistry.register_subscription(
76
- subjects: subjects.flatten,
77
- model: self.name,
78
- handler: handler,
79
- queue_group: queue_group
80
- )
252
+ Rails.logger.debug "📤 #{self.name}: Registered publishing config for subjects: #{subjects}" if defined?(Rails)
81
253
  end
82
254
 
83
- # Auto-generate subjects based on external model pattern
84
- def nats_wave_auto_subjects_for(external_model, service_prefix: nil, actions: [:create, :update, :destroy])
85
- model_name = external_model.underscore
86
-
87
- if service_prefix
88
- subjects = actions.map { |action| "#{service_prefix}.#{model_name}.#{action}" }
89
- subjects << "#{service_prefix}.#{model_name}.*" # Wildcard for all actions
90
- else
91
- # Try to infer from common patterns
92
- subjects = [
93
- "#{model_name}.*", # Simple pattern
94
- "events.#{model_name}.*", # Events pattern
95
- "*.#{model_name}.*", # Service.model pattern
96
- "*.events.#{model_name}.*" # Service.events.model pattern
97
- ]
98
- end
99
-
100
- subjects
255
+ # Get all subscription configurations
256
+ def nats_wave_subscription_configs
257
+ nats_wave_subscription_config
101
258
  end
102
259
 
103
- # Get mapping configuration for an external model
104
- def nats_wave_mapping_for(external_model)
105
- nats_wave_mapping_config[external_model]
260
+ # Get all publishing configurations
261
+ def nats_wave_publishing_configs
262
+ nats_wave_publishing_config
106
263
  end
107
264
 
108
- # Check if this model can sync from an external model
109
- def nats_wave_can_sync_from?(external_model)
110
- nats_wave_mapping_config.key?(external_model)
265
+ # Get all subjects this model subscribes to
266
+ def nats_wave_subscribed_subjects
267
+ nats_wave_subscription_config.values.flat_map { |config| config[:subjects] }.uniq
111
268
  end
112
269
 
113
- # Get all external models this model can sync from
114
- def nats_wave_external_models
115
- nats_wave_mapping_config.keys
270
+ # Get all subjects this model publishes to
271
+ def nats_wave_published_subjects
272
+ nats_wave_publishing_config.values.flat_map { |config| config[:subjects] }.uniq
116
273
  end
117
274
 
118
- # Get all subjects this model subscribes to
119
- def nats_wave_subscribed_subjects
120
- subjects = []
275
+ private
276
+
277
+ # Create a subscription handler that processes data and calls custom handler or auto-sync
278
+ def create_subscription_handler(config)
279
+ model_class = self
121
280
 
122
- # From model mappings
123
- nats_wave_mapping_config.each do |_, config|
124
- subjects.concat(config[:subjects] || [])
281
+ lambda do |message|
282
+ begin
283
+ Rails.logger.debug "📨 Processing subscription message for #{model_class.name}" if defined?(Rails)
284
+
285
+ # Extract the raw data
286
+ raw_data = message['data'] || {}
287
+ model_name = message['model']
288
+ action = message['action']
289
+
290
+ # Process the data through mappings and transformations
291
+ processed_data = model_class.process_data(raw_data, config)
292
+
293
+ # If custom handler is provided, call it with model_name, action, processed_data
294
+ if config[:handler]
295
+ Rails.logger.debug "📨 Calling custom subscription handler" if defined?(Rails)
296
+ config[:handler].call(model_name, action, processed_data, message)
297
+ elsif config[:auto_sync]
298
+ Rails.logger.debug "📨 Performing auto-sync" if defined?(Rails)
299
+ # Default auto-sync behavior
300
+ model_class.perform_auto_sync(model_name, action, processed_data, config)
301
+ else
302
+ Rails.logger.debug "📨 No handler provided and auto_sync disabled" if defined?(Rails)
303
+ end
304
+
305
+ rescue => e
306
+ Rails.logger.error "Error in subscription handler for #{model_class.name}: #{e.message}" if defined?(Rails)
307
+ Rails.logger.error e.backtrace.join("\n") if defined?(Rails)
308
+ raise
309
+ end
125
310
  end
311
+ end
312
+
313
+ # Process data through field mappings and transformations (used by both subscription and publishing)
314
+ def process_data(raw_data, config)
315
+ processed_data = {}
316
+ field_mappings = config[:field_mappings]
317
+ transformations = config[:transformations]
318
+ skip_fields = config[:skip_fields]
126
319
 
127
- # From custom subscriptions
128
- if nats_wave_subscription_config[:custom]
129
- subjects.concat(nats_wave_subscription_config[:custom][:subjects] || [])
320
+ raw_data.each do |field, value|
321
+ field_str = field.to_s
322
+ field_sym = field.to_sym
323
+
324
+ # Skip if in skip_fields
325
+ next if skip_fields.include?(field_str) || skip_fields.include?(field_sym)
326
+
327
+ # Apply field mapping
328
+ mapped_field = field_mappings[field_str] ||
329
+ field_mappings[field_sym] ||
330
+ field
331
+
332
+ # Apply transformation if any
333
+ transformed_value = apply_transformation(value, mapped_field, transformations, raw_data)
334
+
335
+ processed_data[mapped_field.to_s] = transformed_value
130
336
  end
131
337
 
132
- subjects.uniq
338
+ processed_data
133
339
  end
134
- end
135
-
136
- # Instance methods remain the same...
137
- def nats_wave_mapped_attributes_for(target_model)
138
- mapping = self.class.nats_wave_mapping_config[target_model]
139
- return attributes unless mapping
140
340
 
141
- mapped_attrs = {}
142
- field_mappings = mapping[:field_mappings] || {}
143
- skip_fields = mapping[:skip_fields] || []
341
+ # Apply transformations to field values
342
+ def apply_transformation(value, field, transformations, full_record)
343
+ transformation = transformations[field] || transformations[field.to_sym]
344
+
345
+ case transformation
346
+ when Proc
347
+ if transformation.arity == 2 || transformation.arity < 0
348
+ transformation.call(value, full_record)
349
+ else
350
+ transformation.call(value)
351
+ end
352
+ when Symbol
353
+ if self.respond_to?(transformation, true)
354
+ self.send(transformation, value)
355
+ elsif value.respond_to?(transformation)
356
+ value.send(transformation)
357
+ else
358
+ Rails.logger.warn "Transformation method #{transformation} not found" if defined?(Rails)
359
+ value
360
+ end
361
+ else
362
+ value
363
+ end
364
+ end
144
365
 
145
- attributes.each do |key, value|
146
- next if skip_fields.include?(key.to_s) || skip_fields.include?(key.to_sym)
366
+ # Default auto-sync behavior
367
+ def perform_auto_sync(model_name, action, processed_data, config)
368
+ unique_fields = config[:unique_fields]
369
+ sync_strategy = config[:sync_strategy]
147
370
 
148
- mapped_key = field_mappings[key] || field_mappings[key.to_sym] || key
149
- mapped_attrs[mapped_key] = transform_value(value, mapped_key, mapping[:transformations] || {})
371
+ case action.to_s.downcase
372
+ when 'create', 'created'
373
+ handle_auto_create(processed_data, unique_fields, sync_strategy)
374
+ when 'update', 'updated'
375
+ handle_auto_update(processed_data, unique_fields, sync_strategy)
376
+ when 'delete', 'deleted', 'destroy', 'destroyed'
377
+ handle_auto_delete(processed_data, unique_fields)
378
+ else
379
+ Rails.logger.warn "Unknown action for auto-sync: #{action}" if defined?(Rails)
380
+ end
381
+ end
382
+
383
+ def handle_auto_create(data, unique_fields, sync_strategy)
384
+ case sync_strategy
385
+ when :upsert
386
+ existing = find_by_unique_fields(data, unique_fields)
387
+ if existing
388
+ existing.update!(data)
389
+ Rails.logger.info "✅ Updated existing #{self.name}: #{existing.id}" if defined?(Rails)
390
+ else
391
+ record = self.create!(data)
392
+ Rails.logger.info "✅ Created new #{self.name}: #{record.id}" if defined?(Rails)
393
+ end
394
+ when :create_only
395
+ record = self.create!(data)
396
+ Rails.logger.info "✅ Created new #{self.name}: #{record.id}" if defined?(Rails)
397
+ end
398
+ rescue => e
399
+ Rails.logger.error "❌ Failed to create #{self.name}: #{e.message}" if defined?(Rails)
400
+ raise
150
401
  end
151
402
 
152
- mapped_attrs
403
+ def handle_auto_update(data, unique_fields, sync_strategy)
404
+ existing = find_by_unique_fields(data, unique_fields)
405
+ if existing
406
+ existing.update!(data)
407
+ Rails.logger.info "✅ Updated #{self.name}: #{existing.id}" if defined?(Rails)
408
+ elsif sync_strategy == :upsert
409
+ record = self.create!(data)
410
+ Rails.logger.info "✅ Created new #{self.name} during update: #{record.id}" if defined?(Rails)
411
+ else
412
+ Rails.logger.warn "⚠️ Record not found for update: #{data.slice(*unique_fields.map(&:to_s))}" if defined?(Rails)
413
+ end
414
+ rescue => e
415
+ Rails.logger.error "❌ Failed to update #{self.name}: #{e.message}" if defined?(Rails)
416
+ raise
417
+ end
418
+
419
+ def handle_auto_delete(data, unique_fields)
420
+ existing = find_by_unique_fields(data, unique_fields)
421
+ if existing
422
+ existing.destroy!
423
+ Rails.logger.info "✅ Deleted #{self.name}: #{existing.id}" if defined?(Rails)
424
+ else
425
+ Rails.logger.warn "⚠️ Record not found for deletion: #{data.slice(*unique_fields.map(&:to_s))}" if defined?(Rails)
426
+ end
427
+ rescue => e
428
+ Rails.logger.error "❌ Failed to delete #{self.name}: #{e.message}" if defined?(Rails)
429
+ raise
430
+ end
431
+
432
+ def find_by_unique_fields(data, unique_fields)
433
+ conditions = {}
434
+ unique_fields.each do |field|
435
+ field_str = field.to_s
436
+ if data.key?(field_str)
437
+ conditions[field] = data[field_str]
438
+ elsif data.key?(field.to_sym)
439
+ conditions[field] = data[field.to_sym]
440
+ end
441
+ end
442
+
443
+ return nil if conditions.empty?
444
+ self.find_by(conditions)
445
+ end
153
446
  end
154
447
 
155
- def nats_wave_unique_identifier_for(external_model)
156
- mapping = self.class.nats_wave_mapping_config[external_model]
157
- unique_fields = mapping&.dig(:unique_fields) || [:id]
448
+ # Instance methods for publishing
449
+ def nats_wave_publish(action = nil)
450
+ action ||= determine_action_from_context
158
451
 
159
- unique_fields.map { |field| [field, send(field)] }.to_h
452
+ self.class.nats_wave_publishing_configs.each do |subjects_key, config|
453
+ next unless config[:enabled]
454
+ next unless config[:actions].include?(action.to_sym)
455
+
456
+ # Check conditions (only for publishing)
457
+ if config[:conditions].any? && !evaluate_conditions(config[:conditions])
458
+ Rails.logger.debug "📤 Skipping publish - conditions not met" if defined?(Rails)
459
+ next
460
+ end
461
+
462
+ # Get the raw data (current model attributes)
463
+ raw_data = get_raw_attributes
464
+
465
+ # Process the data through mappings and transformations (same as subscription)
466
+ processed_data = self.class.process_data(raw_data, config)
467
+
468
+ # If custom handler is provided, call it
469
+ if config[:handler]
470
+ Rails.logger.debug "📤 Calling custom publishing handler" if defined?(Rails)
471
+ config[:handler].call(self.class.name, action, processed_data, self)
472
+ else
473
+ # Default publishing behavior - publish the processed data
474
+ config[:subjects].each do |subject|
475
+ NatsWave.client.publish(
476
+ subject: subject,
477
+ model: self.class.name,
478
+ action: action,
479
+ data: processed_data, # This is the processed data, not raw attributes
480
+ metadata: build_publishing_metadata
481
+ )
482
+
483
+ Rails.logger.info "📤 Published #{self.class.name}##{id} to #{subject} (#{action})" if defined?(Rails)
484
+ end
485
+ end
486
+ end
487
+ rescue StandardError => e
488
+ Rails.logger.error("Failed to publish: #{e.message}") if defined?(Rails)
489
+ # Don't re-raise to avoid breaking transactions
160
490
  end
161
491
 
162
492
  private
163
493
 
164
- def transform_value(value, field, transformations)
165
- transformation = transformations[field] || transformations[field.to_sym]
166
-
167
- case transformation
168
- when Proc
169
- transformation.call(value)
170
- when Symbol
171
- send(transformation, value) if respond_to?(transformation, true)
172
- else
173
- value
494
+ def determine_action_from_context
495
+ # Try to determine action from ActiveRecord context
496
+ if defined?(ActiveRecord) && is_a?(ActiveRecord::Base)
497
+ return 'create' if previously_new_record?
498
+ return 'update' if saved_changes.any?
499
+ return 'destroy' if destroyed?
174
500
  end
501
+
502
+ 'update' # Default fallback
503
+ end
504
+
505
+ def get_raw_attributes
506
+ # Get attributes (works for both ActiveRecord and other objects)
507
+ if respond_to?(:attributes)
508
+ attributes
509
+ else
510
+ # For non-ActiveRecord objects, get instance variables
511
+ instance_variables.map { |v| [v.to_s.delete('@'), instance_variable_get(v)] }.to_h
512
+ end
513
+ end
514
+
515
+ def evaluate_conditions(conditions)
516
+ conditions.all? do |condition, expected_value|
517
+ case condition
518
+ when Proc
519
+ condition.call(self)
520
+ when Symbol, String
521
+ if respond_to?(condition, true)
522
+ result = send(condition)
523
+ expected_value.nil? ? result : result == expected_value
524
+ else
525
+ false
526
+ end
527
+ else
528
+ false
529
+ end
530
+ end
531
+ end
532
+
533
+ def build_publishing_metadata
534
+ {
535
+ source_model: self.class.name,
536
+ source_id: respond_to?(:id) ? id : object_id,
537
+ published_at: Time.current.iso8601
538
+ }
175
539
  end
176
540
  end
177
541
  end