nats_wave 1.1.11 → 1.1.13

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,822 @@
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_subscription_config, :nats_wave_publishing_config
10
+ # # # self.nats_wave_subscription_config = {}
11
+ # # # self.nats_wave_publishing_config = {}
12
+ # # # end
13
+ # # #
14
+ # # # class_methods do
15
+ # # # # Configure subscriptions with optional custom handler
16
+ # # # def nats_wave_subscribes_to(*subjects, **options, &block)
17
+ # # # Rails.logger.debug "📡 #{self.name}: Setting up subscription to subjects: #{subjects.inspect}" if defined?(Rails)
18
+ # # #
19
+ # # # handler = options[:handler] || block
20
+ # # #
21
+ # # # subscription_config = {
22
+ # # # subjects: subjects.flatten,
23
+ # # # field_mappings: options[:field_mappings] || {},
24
+ # # # transformations: options[:transformations] || {},
25
+ # # # skip_fields: options[:skip_fields] || [],
26
+ # # # unique_fields: options[:unique_fields] || [:id],
27
+ # # # sync_strategy: options[:sync_strategy] || :upsert,
28
+ # # # handler: handler,
29
+ # # # queue_group: options[:queue_group],
30
+ # # # auto_sync: options[:auto_sync] != false # Default to true, can be disabled
31
+ # # # }
32
+ # # #
33
+ # # # # Store the config
34
+ # # # config_key = subjects.join(',')
35
+ # # # self.nats_wave_subscription_config[config_key] = subscription_config
36
+ # # #
37
+ # # # # Create the subscription handler
38
+ # # # processed_handler = create_subscription_handler(subscription_config)
39
+ # # #
40
+ # # # # Register the subscription
41
+ # # # NatsWave::ModelRegistry.register_subscription(
42
+ # # # subjects: subjects.flatten,
43
+ # # # model: self.name,
44
+ # # # handler: processed_handler,
45
+ # # # queue_group: subscription_config[:queue_group]
46
+ # # # )
47
+ # # #
48
+ # # # Rails.logger.debug "📡 #{self.name}: Registered subscription for subjects: #{subjects}" if defined?(Rails)
49
+ # # # end
50
+ # # #
51
+ # # # # Configure publishing with optional custom handler
52
+ # # # def nats_wave_publishes_to(*subjects, **options, &block)
53
+ # # # Rails.logger.debug "📤 #{self.name}: Setting up publishing to subjects: #{subjects.inspect}" if defined?(Rails)
54
+ # # #
55
+ # # # handler = options[:handler] || block
56
+ # # #
57
+ # # # publishing_config = {
58
+ # # # subjects: subjects.flatten,
59
+ # # # field_mappings: options[:field_mappings] || {},
60
+ # # # transformations: options[:transformations] || {},
61
+ # # # skip_fields: options[:skip_fields] || [],
62
+ # # # conditions: options[:conditions] || {},
63
+ # # # actions: options[:actions] || [:create, :update, :destroy],
64
+ # # # handler: handler,
65
+ # # # async: options[:async] != false, # Default to true
66
+ # # # enabled: options[:enabled] != false # Default to true
67
+ # # # }
68
+ # # #
69
+ # # # # Store the config
70
+ # # # config_key = subjects.join(',')
71
+ # # # self.nats_wave_publishing_config[config_key] = publishing_config
72
+ # # #
73
+ # # # Rails.logger.debug "📤 #{self.name}: Registered publishing config for subjects: #{subjects}" if defined?(Rails)
74
+ # # # end
75
+ # # #
76
+ # # # # Get all subscription configurations
77
+ # # # def nats_wave_subscription_configs
78
+ # # # nats_wave_subscription_config
79
+ # # # end
80
+ # # #
81
+ # # # # Get all publishing configurations
82
+ # # # def nats_wave_publishing_configs
83
+ # # # nats_wave_publishing_config
84
+ # # # end
85
+ # # #
86
+ # # # # Get all subjects this model subscribes to
87
+ # # # def nats_wave_subscribed_subjects
88
+ # # # nats_wave_subscription_config.values.flat_map { |config| config[:subjects] }.uniq
89
+ # # # end
90
+ # # #
91
+ # # # # Get all subjects this model publishes to
92
+ # # # def nats_wave_published_subjects
93
+ # # # nats_wave_publishing_config.values.flat_map { |config| config[:subjects] }.uniq
94
+ # # # end
95
+ # # #
96
+ # # # private
97
+ # # #
98
+ # # # # Create a subscription handler that processes data and calls custom handler or auto-sync
99
+ # # # def create_subscription_handler(config)
100
+ # # # model_class = self
101
+ # # #
102
+ # # # lambda do |message|
103
+ # # # begin
104
+ # # # Rails.logger.debug "📨 Processing subscription message for #{model_class.name}" if defined?(Rails)
105
+ # # #
106
+ # # # # Extract the raw data
107
+ # # # raw_data = message['data'] || {}
108
+ # # # model_name = message['model']
109
+ # # # action = message['action']
110
+ # # #
111
+ # # # # Process the data through mappings and transformations
112
+ # # # processed_data = model_class.process_data(raw_data, config)
113
+ # # #
114
+ # # # # If custom handler is provided, call it with model_name, action, processed_data
115
+ # # # if config[:handler]
116
+ # # # Rails.logger.debug "📨 Calling custom subscription handler" if defined?(Rails)
117
+ # # # config[:handler].call(model_name, action, processed_data, message)
118
+ # # # elsif config[:auto_sync]
119
+ # # # Rails.logger.debug "📨 Performing auto-sync" if defined?(Rails)
120
+ # # # # Default auto-sync behavior
121
+ # # # model_class.perform_auto_sync(model_name, action, processed_data, config)
122
+ # # # else
123
+ # # # Rails.logger.debug "📨 No handler provided and auto_sync disabled" if defined?(Rails)
124
+ # # # end
125
+ # # #
126
+ # # # rescue => e
127
+ # # # Rails.logger.error "Error in subscription handler for #{model_class.name}: #{e.message}" if defined?(Rails)
128
+ # # # Rails.logger.error e.backtrace.join("\n") if defined?(Rails)
129
+ # # # raise
130
+ # # # end
131
+ # # # end
132
+ # # # end
133
+ # # #
134
+ # # # # Process data through field mappings and transformations (used by both subscription and publishing)
135
+ # # # def process_data(raw_data, config)
136
+ # # # processed_data = {}
137
+ # # # field_mappings = config[:field_mappings]
138
+ # # # transformations = config[:transformations]
139
+ # # # skip_fields = config[:skip_fields]
140
+ # # #
141
+ # # # raw_data.each do |field, value|
142
+ # # # field_str = field.to_s
143
+ # # # field_sym = field.to_sym
144
+ # # #
145
+ # # # # Skip if in skip_fields
146
+ # # # next if skip_fields.include?(field_str) || skip_fields.include?(field_sym)
147
+ # # #
148
+ # # # # Apply field mapping
149
+ # # # mapped_field = field_mappings[field_str] ||
150
+ # # # field_mappings[field_sym] ||
151
+ # # # field
152
+ # # #
153
+ # # # # Apply transformation if any
154
+ # # # transformed_value = apply_transformation(value, mapped_field, transformations, raw_data)
155
+ # # #
156
+ # # # processed_data[mapped_field.to_s] = transformed_value
157
+ # # # end
158
+ # # #
159
+ # # # processed_data
160
+ # # # end
161
+ # # #
162
+ # # # # Apply transformations to field values
163
+ # # # def apply_transformation(value, field, transformations, full_record)
164
+ # # # transformation = transformations[field] || transformations[field.to_sym]
165
+ # # #
166
+ # # # case transformation
167
+ # # # when Proc
168
+ # # # if transformation.arity == 2 || transformation.arity < 0
169
+ # # # transformation.call(value, full_record)
170
+ # # # else
171
+ # # # transformation.call(value)
172
+ # # # end
173
+ # # # when Symbol
174
+ # # # if self.respond_to?(transformation, true)
175
+ # # # self.send(transformation, value)
176
+ # # # elsif value.respond_to?(transformation)
177
+ # # # value.send(transformation)
178
+ # # # else
179
+ # # # Rails.logger.warn "Transformation method #{transformation} not found" if defined?(Rails)
180
+ # # # value
181
+ # # # end
182
+ # # # else
183
+ # # # value
184
+ # # # end
185
+ # # # end
186
+ # # #
187
+ # # # # Default auto-sync behavior
188
+ # # # def perform_auto_sync(model_name, action, processed_data, config)
189
+ # # # unique_fields = config[:unique_fields]
190
+ # # # sync_strategy = config[:sync_strategy]
191
+ # # #
192
+ # # # case action.to_s.downcase
193
+ # # # when 'create', 'created'
194
+ # # # handle_auto_create(processed_data, unique_fields, sync_strategy)
195
+ # # # when 'update', 'updated'
196
+ # # # handle_auto_update(processed_data, unique_fields, sync_strategy)
197
+ # # # when 'delete', 'deleted', 'destroy', 'destroyed'
198
+ # # # handle_auto_delete(processed_data, unique_fields)
199
+ # # # else
200
+ # # # Rails.logger.warn "Unknown action for auto-sync: #{action}" if defined?(Rails)
201
+ # # # end
202
+ # # # end
203
+ # # #
204
+ # # # def handle_auto_create(data, unique_fields, sync_strategy)
205
+ # # # case sync_strategy
206
+ # # # when :upsert
207
+ # # # existing = find_by_unique_fields(data, unique_fields)
208
+ # # # if existing
209
+ # # # existing.update!(data)
210
+ # # # Rails.logger.info "✅ Updated existing #{self.name}: #{existing.id}" if defined?(Rails)
211
+ # # # else
212
+ # # # record = self.create!(data)
213
+ # # # Rails.logger.info "✅ Created new #{self.name}: #{record.id}" if defined?(Rails)
214
+ # # # end
215
+ # # # when :create_only
216
+ # # # record = self.create!(data)
217
+ # # # Rails.logger.info "✅ Created new #{self.name}: #{record.id}" if defined?(Rails)
218
+ # # # end
219
+ # # # rescue => e
220
+ # # # Rails.logger.error "❌ Failed to create #{self.name}: #{e.message}" if defined?(Rails)
221
+ # # # raise
222
+ # # # end
223
+ # # #
224
+ # # # def handle_auto_update(data, unique_fields, sync_strategy)
225
+ # # # existing = find_by_unique_fields(data, unique_fields)
226
+ # # # if existing
227
+ # # # existing.update!(data)
228
+ # # # Rails.logger.info "✅ Updated #{self.name}: #{existing.id}" if defined?(Rails)
229
+ # # # elsif sync_strategy == :upsert
230
+ # # # record = self.create!(data)
231
+ # # # Rails.logger.info "✅ Created new #{self.name} during update: #{record.id}" if defined?(Rails)
232
+ # # # else
233
+ # # # Rails.logger.warn "⚠️ Record not found for update: #{data.slice(*unique_fields.map(&:to_s))}" if defined?(Rails)
234
+ # # # end
235
+ # # # rescue => e
236
+ # # # Rails.logger.error "❌ Failed to update #{self.name}: #{e.message}" if defined?(Rails)
237
+ # # # raise
238
+ # # # end
239
+ # # #
240
+ # # # def handle_auto_delete(data, unique_fields)
241
+ # # # existing = find_by_unique_fields(data, unique_fields)
242
+ # # # if existing
243
+ # # # existing.destroy!
244
+ # # # Rails.logger.info "✅ Deleted #{self.name}: #{existing.id}" if defined?(Rails)
245
+ # # # else
246
+ # # # Rails.logger.warn "⚠️ Record not found for deletion: #{data.slice(*unique_fields.map(&:to_s))}" if defined?(Rails)
247
+ # # # end
248
+ # # # rescue => e
249
+ # # # Rails.logger.error "❌ Failed to delete #{self.name}: #{e.message}" if defined?(Rails)
250
+ # # # raise
251
+ # # # end
252
+ # # #
253
+ # # # def find_by_unique_fields(data, unique_fields)
254
+ # # # conditions = {}
255
+ # # # unique_fields.each do |field|
256
+ # # # field_str = field.to_s
257
+ # # # if data.key?(field_str)
258
+ # # # conditions[field] = data[field_str]
259
+ # # # elsif data.key?(field.to_sym)
260
+ # # # conditions[field] = data[field.to_sym]
261
+ # # # end
262
+ # # # end
263
+ # # #
264
+ # # # return nil if conditions.empty?
265
+ # # # self.find_by(conditions)
266
+ # # # end
267
+ # # # end
268
+ # # #
269
+ # # # # Instance methods for publishing
270
+ # # # def nats_wave_publish(action = nil)
271
+ # # # action ||= determine_action_from_context
272
+ # # #
273
+ # # # self.class.nats_wave_publishing_configs.each do |subjects_key, config|
274
+ # # # next unless config[:enabled]
275
+ # # # next unless config[:actions].include?(action.to_sym)
276
+ # # #
277
+ # # # # Check conditions (only for publishing)
278
+ # # # if config[:conditions].any? && !evaluate_conditions(config[:conditions])
279
+ # # # Rails.logger.debug "📤 Skipping publish - conditions not met" if defined?(Rails)
280
+ # # # next
281
+ # # # end
282
+ # # #
283
+ # # # # Get the raw data (current model attributes)
284
+ # # # raw_data = get_raw_attributes
285
+ # # #
286
+ # # # # Process the data through mappings and transformations (same as subscription)
287
+ # # # processed_data = self.class.process_data(raw_data, config)
288
+ # # #
289
+ # # # # If custom handler is provided, call it
290
+ # # # if config[:handler]
291
+ # # # Rails.logger.debug "📤 Calling custom publishing handler" if defined?(Rails)
292
+ # # # config[:handler].call(self.class.name, action, processed_data, self)
293
+ # # # else
294
+ # # # # Default publishing behavior - publish the processed data
295
+ # # # config[:subjects].each do |subject|
296
+ # # # NatsWave.client.publish(
297
+ # # # subject: subject,
298
+ # # # model: self.class.name,
299
+ # # # action: action,
300
+ # # # data: processed_data, # This is the processed data, not raw attributes
301
+ # # # metadata: build_publishing_metadata
302
+ # # # )
303
+ # # #
304
+ # # # Rails.logger.info "📤 Published #{self.class.name}##{id} to #{subject} (#{action})" if defined?(Rails)
305
+ # # # end
306
+ # # # end
307
+ # # # end
308
+ # # # rescue StandardError => e
309
+ # # # Rails.logger.error("Failed to publish: #{e.message}") if defined?(Rails)
310
+ # # # # Don't re-raise to avoid breaking transactions
311
+ # # # end
312
+ # # #
313
+ # # # private
314
+ # # #
315
+ # # # def determine_action_from_context
316
+ # # # # Try to determine action from ActiveRecord context
317
+ # # # if defined?(ActiveRecord) && is_a?(ActiveRecord::Base)
318
+ # # # return 'create' if previously_new_record?
319
+ # # # return 'update' if saved_changes.any?
320
+ # # # return 'destroy' if destroyed?
321
+ # # # end
322
+ # # #
323
+ # # # 'update' # Default fallback
324
+ # # # end
325
+ # # #
326
+ # # # def get_raw_attributes
327
+ # # # # Get attributes (works for both ActiveRecord and other objects)
328
+ # # # if respond_to?(:attributes)
329
+ # # # attributes
330
+ # # # else
331
+ # # # # For non-ActiveRecord objects, get instance variables
332
+ # # # instance_variables.map { |v| [v.to_s.delete('@'), instance_variable_get(v)] }.to_h
333
+ # # # end
334
+ # # # end
335
+ # # #
336
+ # # # def evaluate_conditions(conditions)
337
+ # # # conditions.all? do |condition, expected_value|
338
+ # # # case condition
339
+ # # # when Proc
340
+ # # # condition.call(self)
341
+ # # # when Symbol, String
342
+ # # # if respond_to?(condition, true)
343
+ # # # result = send(condition)
344
+ # # # expected_value.nil? ? result : result == expected_value
345
+ # # # else
346
+ # # # false
347
+ # # # end
348
+ # # # else
349
+ # # # false
350
+ # # # end
351
+ # # # end
352
+ # # # end
353
+ # # #
354
+ # # # def build_publishing_metadata
355
+ # # # {
356
+ # # # source_model: self.class.name,
357
+ # # # source_id: respond_to?(:id) ? id : object_id,
358
+ # # # published_at: Time.current.iso8601
359
+ # # # }
360
+ # # # end
361
+ # # # end
362
+ # # # end
363
+ # # # end
364
+ # #
365
+ # # # frozen_string_literal: true
366
+ # #
367
+ # # module NatsWave
368
+ # # module Concerns
369
+ # # module Mappable
370
+ # # extend ActiveSupport::Concern
371
+ # #
372
+ # # included do
373
+ # # class_attribute :nats_wave_subscription_config, :nats_wave_publishing_config
374
+ # # self.nats_wave_subscription_config = {}
375
+ # # self.nats_wave_publishing_config = {}
376
+ # # end
377
+ # #
378
+ # # class_methods do
379
+ # # # Configure subscriptions with optional custom handler
380
+ # # def nats_wave_subscribes_to(*subjects, **options, &block)
381
+ # # Rails.logger.debug "📡 #{self.name}: Setting up subscription to subjects: #{subjects.inspect}" if defined?(Rails)
382
+ # #
383
+ # # handler = options[:handler] || block
384
+ # #
385
+ # # subscription_config = {
386
+ # # subjects: subjects.flatten,
387
+ # # field_mappings: options[:field_mappings] || {},
388
+ # # transformations: options[:transformations] || {},
389
+ # # skip_fields: options[:skip_fields] || [],
390
+ # # unique_fields: options[:unique_fields] || [:id],
391
+ # # sync_strategy: options[:sync_strategy] || :upsert,
392
+ # # handler: handler,
393
+ # # queue_group: options[:queue_group],
394
+ # # auto_sync: options[:auto_sync] != false # Default to true, can be disabled
395
+ # # }
396
+ # #
397
+ # # # Store the config
398
+ # # config_key = subjects.join(',')
399
+ # # self.nats_wave_subscription_config[config_key] = subscription_config
400
+ # #
401
+ # # # Create the subscription handler
402
+ # # processed_handler = create_subscription_handler(subscription_config)
403
+ # #
404
+ # # # Register the subscription
405
+ # # NatsWave::ModelRegistry.register_subscription(
406
+ # # subjects: subjects.flatten,
407
+ # # model: self.name,
408
+ # # handler: processed_handler,
409
+ # # queue_group: subscription_config[:queue_group]
410
+ # # )
411
+ # #
412
+ # # Rails.logger.debug "📡 #{self.name}: Registered subscription for subjects: #{subjects}" if defined?(Rails)
413
+ # # end
414
+ # #
415
+ # # # Configure publishing with optional custom handler AND auto-publishing callbacks
416
+ # # def nats_wave_publishes_to(*subjects, **options, &block)
417
+ # # Rails.logger.debug "📤 #{self.name}: Setting up publishing to subjects: #{subjects.inspect}" if defined?(Rails)
418
+ # #
419
+ # # handler = options[:handler] || block
420
+ # #
421
+ # # publishing_config = {
422
+ # # subjects: subjects.flatten,
423
+ # # field_mappings: options[:field_mappings] || {},
424
+ # # transformations: options[:transformations] || {},
425
+ # # skip_fields: options[:skip_fields] || [],
426
+ # # conditions: options[:conditions] || {},
427
+ # # actions: options[:actions] || [:create, :update, :destroy],
428
+ # # handler: handler,
429
+ # # async: options[:async] != false, # Default to true
430
+ # # enabled: options[:enabled] != false # Default to true
431
+ # # }
432
+ # #
433
+ # # # Store the config
434
+ # # config_key = subjects.join(',')
435
+ # # self.nats_wave_publishing_config[config_key] = publishing_config
436
+ # #
437
+ # # # Add ActiveRecord callbacks for auto-publishing
438
+ # # setup_publishing_callbacks(publishing_config)
439
+ # #
440
+ # # Rails.logger.debug "📤 #{self.name}: Registered publishing config for subjects: #{subjects}" if defined?(Rails)
441
+ # # end
442
+ # #
443
+ # # # Get all subscription configurations
444
+ # # def nats_wave_subscription_configs
445
+ # # nats_wave_subscription_config
446
+ # # end
447
+ # #
448
+ # # # Get all publishing configurations
449
+ # # def nats_wave_publishing_configs
450
+ # # nats_wave_publishing_config
451
+ # # end
452
+ # #
453
+ # # # Get all subjects this model subscribes to
454
+ # # def nats_wave_subscribed_subjects
455
+ # # nats_wave_subscription_config.values.flat_map { |config| config[:subjects] }.uniq
456
+ # # end
457
+ # #
458
+ # # # Get all subjects this model publishes to
459
+ # # def nats_wave_published_subjects
460
+ # # nats_wave_publishing_config.values.flat_map { |config| config[:subjects] }.uniq
461
+ # # end
462
+ # #
463
+ # # private
464
+ # #
465
+ # # # Setup ActiveRecord callbacks for auto-publishing
466
+ # # def setup_publishing_callbacks(config)
467
+ # # # Only add callbacks if we're in ActiveRecord and actions are specified
468
+ # # return unless defined?(ActiveRecord::Base) && self < ActiveRecord::Base
469
+ # #
470
+ # # actions = config[:actions]
471
+ # #
472
+ # # if actions.include?(:create)
473
+ # # after_commit :trigger_nats_wave_auto_publish_on_create, on: :create
474
+ # # end
475
+ # #
476
+ # # if actions.include?(:update)
477
+ # # after_commit :trigger_nats_wave_auto_publish_on_update, on: :update
478
+ # # end
479
+ # #
480
+ # # if actions.include?(:destroy)
481
+ # # after_commit :trigger_nats_wave_auto_publish_on_destroy, on: :destroy
482
+ # # end
483
+ # # end
484
+ # #
485
+ # # # Create a subscription handler that processes data and calls custom handler or auto-sync
486
+ # # def create_subscription_handler(config)
487
+ # # model_class = self
488
+ # #
489
+ # # lambda do |message|
490
+ # # begin
491
+ # # Rails.logger.debug "📨 Processing subscription message for #{model_class.name}" if defined?(Rails)
492
+ # #
493
+ # # # Extract the raw data
494
+ # # raw_data = message['data'] || {}
495
+ # # model_name = message['model']
496
+ # # action = message['action']
497
+ # #
498
+ # # # Process the data through mappings and transformations
499
+ # # processed_data = process_subscription_data(raw_data, config)
500
+ # #
501
+ # # # If custom handler is provided, call it with model_name, action, processed_data
502
+ # # if config[:handler]
503
+ # # Rails.logger.debug "📨 Calling custom subscription handler" if defined?(Rails)
504
+ # # config[:handler].call(model_name, action, processed_data, message)
505
+ # # elsif config[:auto_sync]
506
+ # # Rails.logger.debug "📨 Performing auto-sync" if defined?(Rails)
507
+ # # # Default auto-sync behavior
508
+ # # model_class.perform_auto_sync(model_name, action, processed_data, config)
509
+ # # else
510
+ # # Rails.logger.debug "📨 No handler provided and auto_sync disabled" if defined?(Rails)
511
+ # # end
512
+ # #
513
+ # # rescue => e
514
+ # # Rails.logger.error "Error in subscription handler for #{model_class.name}: #{e.message}" if defined?(Rails)
515
+ # # Rails.logger.error e.backtrace.join("\n") if defined?(Rails)
516
+ # # raise
517
+ # # end
518
+ # # end
519
+ # # end
520
+ # #
521
+ # # # Process subscription data through field mappings and transformations (CLASS METHOD)
522
+ # # def process_subscription_data(raw_data, config)
523
+ # # processed_data = {}
524
+ # # field_mappings = config[:field_mappings]
525
+ # # transformations = config[:transformations]
526
+ # # skip_fields = config[:skip_fields]
527
+ # #
528
+ # # raw_data.each do |field, value|
529
+ # # field_str = field.to_s
530
+ # # field_sym = field.to_sym
531
+ # #
532
+ # # # Skip if in skip_fields
533
+ # # next if skip_fields.include?(field_str) || skip_fields.include?(field_sym)
534
+ # #
535
+ # # # Apply field mapping
536
+ # # mapped_field = field_mappings[field_str] ||
537
+ # # field_mappings[field_sym] ||
538
+ # # field
539
+ # #
540
+ # # # Apply transformation if any
541
+ # # transformed_value = apply_transformation(value, mapped_field, transformations, raw_data)
542
+ # #
543
+ # # processed_data[mapped_field.to_s] = transformed_value
544
+ # # end
545
+ # #
546
+ # # processed_data
547
+ # # end
548
+ # #
549
+ # # # Apply transformations to field values (CLASS METHOD)
550
+ # # def apply_transformation(value, field, transformations, full_record)
551
+ # # transformation = transformations[field] || transformations[field.to_sym]
552
+ # #
553
+ # # case transformation
554
+ # # when Proc
555
+ # # if transformation.arity == 2 || transformation.arity < 0
556
+ # # transformation.call(value, full_record)
557
+ # # else
558
+ # # transformation.call(value)
559
+ # # end
560
+ # # when Symbol
561
+ # # if self.respond_to?(transformation, true)
562
+ # # self.send(transformation, value)
563
+ # # elsif value.respond_to?(transformation)
564
+ # # value.send(transformation)
565
+ # # else
566
+ # # Rails.logger.warn "Transformation method #{transformation} not found" if defined?(Rails)
567
+ # # value
568
+ # # end
569
+ # # else
570
+ # # value
571
+ # # end
572
+ # # end
573
+ # #
574
+ # # # Default auto-sync behavior (CLASS METHOD)
575
+ # # def perform_auto_sync(model_name, action, processed_data, config)
576
+ # # unique_fields = config[:unique_fields]
577
+ # # sync_strategy = config[:sync_strategy]
578
+ # #
579
+ # # case action.to_s.downcase
580
+ # # when 'create', 'created'
581
+ # # handle_auto_create(processed_data, unique_fields, sync_strategy)
582
+ # # when 'update', 'updated'
583
+ # # handle_auto_update(processed_data, unique_fields, sync_strategy)
584
+ # # when 'delete', 'deleted', 'destroy', 'destroyed'
585
+ # # handle_auto_delete(processed_data, unique_fields)
586
+ # # else
587
+ # # Rails.logger.warn "Unknown action for auto-sync: #{action}" if defined?(Rails)
588
+ # # end
589
+ # # end
590
+ # #
591
+ # # def handle_auto_create(data, unique_fields, sync_strategy)
592
+ # # case sync_strategy
593
+ # # when :upsert
594
+ # # existing = find_by_unique_fields(data, unique_fields)
595
+ # # if existing
596
+ # # existing.update!(data)
597
+ # # Rails.logger.info "✅ Updated existing #{self.name}: #{existing.id}" if defined?(Rails)
598
+ # # else
599
+ # # record = self.create!(data)
600
+ # # Rails.logger.info "✅ Created new #{self.name}: #{record.id}" if defined?(Rails)
601
+ # # end
602
+ # # when :create_only
603
+ # # record = self.create!(data)
604
+ # # Rails.logger.info "✅ Created new #{self.name}: #{record.id}" if defined?(Rails)
605
+ # # end
606
+ # # rescue => e
607
+ # # Rails.logger.error "❌ Failed to create #{self.name}: #{e.message}" if defined?(Rails)
608
+ # # raise
609
+ # # end
610
+ # #
611
+ # # def handle_auto_update(data, unique_fields, sync_strategy)
612
+ # # existing = find_by_unique_fields(data, unique_fields)
613
+ # # if existing
614
+ # # existing.update!(data)
615
+ # # Rails.logger.info "✅ Updated #{self.name}: #{existing.id}" if defined?(Rails)
616
+ # # elsif sync_strategy == :upsert
617
+ # # record = self.create!(data)
618
+ # # Rails.logger.info "✅ Created new #{self.name} during update: #{record.id}" if defined?(Rails)
619
+ # # else
620
+ # # Rails.logger.warn "⚠️ Record not found for update: #{data.slice(*unique_fields.map(&:to_s))}" if defined?(Rails)
621
+ # # end
622
+ # # rescue => e
623
+ # # Rails.logger.error "❌ Failed to update #{self.name}: #{e.message}" if defined?(Rails)
624
+ # # raise
625
+ # # end
626
+ # #
627
+ # # def handle_auto_delete(data, unique_fields)
628
+ # # existing = find_by_unique_fields(data, unique_fields)
629
+ # # if existing
630
+ # # existing.destroy!
631
+ # # Rails.logger.info "✅ Deleted #{self.name}: #{existing.id}" if defined?(Rails)
632
+ # # else
633
+ # # Rails.logger.warn "⚠️ Record not found for deletion: #{data.slice(*unique_fields.map(&:to_s))}" if defined?(Rails)
634
+ # # end
635
+ # # rescue => e
636
+ # # Rails.logger.error "❌ Failed to delete #{self.name}: #{e.message}" if defined?(Rails)
637
+ # # raise
638
+ # # end
639
+ # #
640
+ # # def find_by_unique_fields(data, unique_fields)
641
+ # # conditions = {}
642
+ # # unique_fields.each do |field|
643
+ # # field_str = field.to_s
644
+ # # if data.key?(field_str)
645
+ # # conditions[field] = data[field_str]
646
+ # # elsif data.key?(field.to_sym)
647
+ # # conditions[field] = data[field.to_sym]
648
+ # # end
649
+ # # end
650
+ # #
651
+ # # return nil if conditions.empty?
652
+ # # self.find_by(conditions)
653
+ # # end
654
+ # # end
655
+ # #
656
+ # # # Instance methods for publishing
657
+ # # def nats_wave_publish(action = nil)
658
+ # # action ||= determine_action_from_context
659
+ # #
660
+ # # self.class.nats_wave_publishing_configs.each do |subjects_key, config|
661
+ # # next unless config[:enabled]
662
+ # # next unless config[:actions].include?(action.to_sym)
663
+ # #
664
+ # # # Check conditions (only for publishing)
665
+ # # if config[:conditions].any? && !evaluate_conditions(config[:conditions])
666
+ # # Rails.logger.debug "📤 Skipping publish - conditions not met" if defined?(Rails)
667
+ # # next
668
+ # # end
669
+ # #
670
+ # # # Get the raw data (current model attributes)
671
+ # # raw_data = get_raw_attributes
672
+ # #
673
+ # # # Process the data through mappings and transformations
674
+ # # processed_data = process_publishing_data(raw_data, config)
675
+ # #
676
+ # # # If custom handler is provided, call it
677
+ # # if config[:handler]
678
+ # # Rails.logger.debug "📤 Calling custom publishing handler" if defined?(Rails)
679
+ # # config[:handler].call(self.class.name, action, processed_data, self)
680
+ # # else
681
+ # # # Default publishing behavior - publish the processed data
682
+ # # config[:subjects].each do |subject|
683
+ # # NatsWave.client.publish(
684
+ # # subject: subject,
685
+ # # model: self.class.name,
686
+ # # action: action,
687
+ # # data: processed_data, # This is the processed data, not raw attributes
688
+ # # metadata: build_publishing_metadata
689
+ # # )
690
+ # #
691
+ # # Rails.logger.info "📤 Published #{self.class.name}##{id} to #{subject} (#{action})" if defined?(Rails)
692
+ # # end
693
+ # # end
694
+ # # end
695
+ # # rescue StandardError => e
696
+ # # Rails.logger.error("Failed to publish: #{e.message}") if defined?(Rails)
697
+ # # # Don't re-raise to avoid breaking transactions
698
+ # # end
699
+ # #
700
+ # # # Auto-publishing callback methods
701
+ # # def trigger_nats_wave_auto_publish_on_create
702
+ # # Rails.logger.debug "🚀 Auto-publishing on create" if defined?(Rails)
703
+ # # nats_wave_publish('create')
704
+ # # end
705
+ # #
706
+ # # def trigger_nats_wave_auto_publish_on_update
707
+ # # Rails.logger.debug "🚀 Auto-publishing on update" if defined?(Rails)
708
+ # # nats_wave_publish('update')
709
+ # # end
710
+ # #
711
+ # # def trigger_nats_wave_auto_publish_on_destroy
712
+ # # Rails.logger.debug "🚀 Auto-publishing on destroy" if defined?(Rails)
713
+ # # nats_wave_publish('destroy')
714
+ # # end
715
+ # #
716
+ # # private
717
+ # #
718
+ # # def determine_action_from_context
719
+ # # # Try to determine action from ActiveRecord context
720
+ # # if defined?(ActiveRecord) && is_a?(ActiveRecord::Base)
721
+ # # return 'create' if previously_new_record?
722
+ # # return 'update' if saved_changes.any?
723
+ # # return 'destroy' if destroyed?
724
+ # # end
725
+ # #
726
+ # # 'update' # Default fallback
727
+ # # end
728
+ # #
729
+ # # def get_raw_attributes
730
+ # # # Get attributes (works for both ActiveRecord and other objects)
731
+ # # if respond_to?(:attributes)
732
+ # # attributes
733
+ # # else
734
+ # # # For non-ActiveRecord objects, get instance variables
735
+ # # instance_variables.map { |v| [v.to_s.delete('@'), instance_variable_get(v)] }.to_h
736
+ # # end
737
+ # # end
738
+ # #
739
+ # # # Process publishing data through field mappings and transformations (INSTANCE METHOD)
740
+ # # def process_publishing_data(raw_data, config)
741
+ # # processed_data = {}
742
+ # # field_mappings = config[:field_mappings]
743
+ # # transformations = config[:transformations]
744
+ # # skip_fields = config[:skip_fields]
745
+ # #
746
+ # # raw_data.each do |field, value|
747
+ # # field_str = field.to_s
748
+ # # field_sym = field.to_sym
749
+ # #
750
+ # # # Skip if in skip_fields
751
+ # # next if skip_fields.include?(field_str) || skip_fields.include?(field_sym)
752
+ # #
753
+ # # # Apply field mapping (local -> external)
754
+ # # mapped_field = field_mappings[field_str] ||
755
+ # # field_mappings[field_sym] ||
756
+ # # field
757
+ # #
758
+ # # # Apply transformation if any
759
+ # # transformed_value = apply_publishing_transformation(value, mapped_field, transformations, raw_data)
760
+ # #
761
+ # # processed_data[mapped_field.to_s] = transformed_value
762
+ # # end
763
+ # #
764
+ # # processed_data
765
+ # # end
766
+ # #
767
+ # # def apply_publishing_transformation(value, field, transformations, full_record)
768
+ # # transformation = transformations[field] || transformations[field.to_sym]
769
+ # #
770
+ # # case transformation
771
+ # # when Proc
772
+ # # if transformation.arity == 2 || transformation.arity < 0
773
+ # # transformation.call(value, full_record)
774
+ # # else
775
+ # # transformation.call(value)
776
+ # # end
777
+ # # when Symbol
778
+ # # if self.respond_to?(transformation, true)
779
+ # # self.send(transformation, value)
780
+ # # elsif value.respond_to?(transformation)
781
+ # # value.send(transformation)
782
+ # # else
783
+ # # Rails.logger.warn "Publishing transformation method #{transformation} not found" if defined?(Rails)
784
+ # # value
785
+ # # end
786
+ # # else
787
+ # # value
788
+ # # end
789
+ # # end
790
+ # #
791
+ # # def evaluate_conditions(conditions)
792
+ # # conditions.all? do |condition, expected_value|
793
+ # # case condition
794
+ # # when Proc
795
+ # # condition.call(self)
796
+ # # when Symbol, String
797
+ # # if respond_to?(condition, true)
798
+ # # result = send(condition)
799
+ # # expected_value.nil? ? result : result == expected_value
800
+ # # else
801
+ # # false
802
+ # # end
803
+ # # else
804
+ # # false
805
+ # # end
806
+ # # end
807
+ # # end
808
+ # #
809
+ # # def build_publishing_metadata
810
+ # # {
811
+ # # source_model: self.class.name,
812
+ # # source_id: respond_to?(:id) ? id : object_id,
813
+ # # published_at: Time.current.iso8601
814
+ # # }
815
+ # # end
816
+ # # end
817
+ # # end
818
+ # # end
819
+ #
1
820
  # # frozen_string_literal: true
2
821
  #
3
822
  # module NatsWave
@@ -16,7 +835,8 @@
16
835
  # def nats_wave_subscribes_to(*subjects, **options, &block)
17
836
  # Rails.logger.debug "📡 #{self.name}: Setting up subscription to subjects: #{subjects.inspect}" if defined?(Rails)
18
837
  #
19
- # handler = options[:handler] || block
838
+ # # Custom handler is now optional and runs AFTER default behavior
839
+ # custom_handler = options[:handler] || block
20
840
  #
21
841
  # subscription_config = {
22
842
  # subjects: subjects.flatten,
@@ -25,7 +845,7 @@
25
845
  # skip_fields: options[:skip_fields] || [],
26
846
  # unique_fields: options[:unique_fields] || [:id],
27
847
  # sync_strategy: options[:sync_strategy] || :upsert,
28
- # handler: handler,
848
+ # custom_handler: custom_handler, # Renamed to be clear it's optional
29
849
  # queue_group: options[:queue_group],
30
850
  # auto_sync: options[:auto_sync] != false # Default to true, can be disabled
31
851
  # }
@@ -48,11 +868,12 @@
48
868
  # Rails.logger.debug "📡 #{self.name}: Registered subscription for subjects: #{subjects}" if defined?(Rails)
49
869
  # end
50
870
  #
51
- # # Configure publishing with optional custom handler
871
+ # # Configure publishing with optional custom handler AND auto-publishing callbacks
52
872
  # def nats_wave_publishes_to(*subjects, **options, &block)
53
873
  # Rails.logger.debug "📤 #{self.name}: Setting up publishing to subjects: #{subjects.inspect}" if defined?(Rails)
54
874
  #
55
- # handler = options[:handler] || block
875
+ # # Custom handler is now optional and runs AFTER default behavior
876
+ # custom_handler = options[:handler] || block
56
877
  #
57
878
  # publishing_config = {
58
879
  # subjects: subjects.flatten,
@@ -61,15 +882,19 @@
61
882
  # skip_fields: options[:skip_fields] || [],
62
883
  # conditions: options[:conditions] || {},
63
884
  # actions: options[:actions] || [:create, :update, :destroy],
64
- # handler: handler,
885
+ # custom_handler: custom_handler, # Renamed to be clear it's optional
65
886
  # async: options[:async] != false, # Default to true
66
- # enabled: options[:enabled] != false # Default to true
887
+ # enabled: options[:enabled] != false, # Default to true
888
+ # only_mapped_fields: options[:only_mapped_fields] != false # Default to true - only send mapped fields
67
889
  # }
68
890
  #
69
891
  # # Store the config
70
892
  # config_key = subjects.join(',')
71
893
  # self.nats_wave_publishing_config[config_key] = publishing_config
72
894
  #
895
+ # # Add ActiveRecord callbacks for auto-publishing (only once per model)
896
+ # setup_publishing_callbacks_once
897
+ #
73
898
  # Rails.logger.debug "📤 #{self.name}: Registered publishing config for subjects: #{subjects}" if defined?(Rails)
74
899
  # end
75
900
  #
@@ -83,19 +908,23 @@
83
908
  # nats_wave_publishing_config
84
909
  # end
85
910
  #
86
- # # Get all subjects this model subscribes to
87
- # def nats_wave_subscribed_subjects
88
- # nats_wave_subscription_config.values.flat_map { |config| config[:subjects] }.uniq
89
- # end
911
+ # private
90
912
  #
91
- # # Get all subjects this model publishes to
92
- # def nats_wave_published_subjects
93
- # nats_wave_publishing_config.values.flat_map { |config| config[:subjects] }.uniq
94
- # end
913
+ # # Setup ActiveRecord callbacks for auto-publishing (only once per model)
914
+ # def setup_publishing_callbacks_once
915
+ # # Only add callbacks if we're in ActiveRecord and haven't added them yet
916
+ # return unless defined?(ActiveRecord::Base) && self < ActiveRecord::Base
917
+ # return if @nats_wave_callbacks_added
95
918
  #
96
- # private
919
+ # after_commit :trigger_nats_wave_auto_publish_on_create, on: :create
920
+ # after_commit :trigger_nats_wave_auto_publish_on_update, on: :update
921
+ # after_commit :trigger_nats_wave_auto_publish_on_destroy, on: :destroy
97
922
  #
98
- # # Create a subscription handler that processes data and calls custom handler or auto-sync
923
+ # @nats_wave_callbacks_added = true
924
+ # Rails.logger.debug "📤 #{self.name}: Added publishing callbacks" if defined?(Rails)
925
+ # end
926
+ #
927
+ # # Create a subscription handler that processes data and calls custom handler AFTER auto-sync
99
928
  # def create_subscription_handler(config)
100
929
  # model_class = self
101
930
  #
@@ -109,18 +938,18 @@
109
938
  # action = message['action']
110
939
  #
111
940
  # # Process the data through mappings and transformations
112
- # processed_data = model_class.process_data(raw_data, config)
113
- #
114
- # # If custom handler is provided, call it with model_name, action, processed_data
115
- # if config[:handler]
116
- # Rails.logger.debug "📨 Calling custom subscription handler" if defined?(Rails)
117
- # config[:handler].call(model_name, action, processed_data, message)
118
- # elsif config[:auto_sync]
119
- # Rails.logger.debug "📨 Performing auto-sync" if defined?(Rails)
120
- # # Default auto-sync behavior
941
+ # processed_data = process_subscription_data(raw_data, config)
942
+ #
943
+ # # Always perform auto-sync first (if enabled)
944
+ # if config[:auto_sync]
945
+ # Rails.logger.debug "📨 Performing auto-sync first" if defined?(Rails)
121
946
  # model_class.perform_auto_sync(model_name, action, processed_data, config)
122
- # else
123
- # Rails.logger.debug "📨 No handler provided and auto_sync disabled" if defined?(Rails)
947
+ # end
948
+ #
949
+ # # Then call custom handler if provided (optional)
950
+ # if config[:custom_handler]
951
+ # Rails.logger.debug "📨 Calling custom subscription handler after auto-sync" if defined?(Rails)
952
+ # config[:custom_handler].call(model_name, action, processed_data, message)
124
953
  # end
125
954
  #
126
955
  # rescue => e
@@ -131,8 +960,8 @@
131
960
  # end
132
961
  # end
133
962
  #
134
- # # Process data through field mappings and transformations (used by both subscription and publishing)
135
- # def process_data(raw_data, config)
963
+ # # Process subscription data through field mappings and transformations (CLASS METHOD)
964
+ # def process_subscription_data(raw_data, config)
136
965
  # processed_data = {}
137
966
  # field_mappings = config[:field_mappings]
138
967
  # transformations = config[:transformations]
@@ -159,7 +988,7 @@
159
988
  # processed_data
160
989
  # end
161
990
  #
162
- # # Apply transformations to field values
991
+ # # Apply transformations to field values (CLASS METHOD)
163
992
  # def apply_transformation(value, field, transformations, full_record)
164
993
  # transformation = transformations[field] || transformations[field.to_sym]
165
994
  #
@@ -184,7 +1013,7 @@
184
1013
  # end
185
1014
  # end
186
1015
  #
187
- # # Default auto-sync behavior
1016
+ # # Default auto-sync behavior (CLASS METHOD)
188
1017
  # def perform_auto_sync(model_name, action, processed_data, config)
189
1018
  # unique_fields = config[:unique_fields]
190
1019
  # sync_strategy = config[:sync_strategy]
@@ -283,26 +1112,29 @@
283
1112
  # # Get the raw data (current model attributes)
284
1113
  # raw_data = get_raw_attributes
285
1114
  #
286
- # # Process the data through mappings and transformations (same as subscription)
287
- # processed_data = self.class.process_data(raw_data, config)
1115
+ # # Process the data through mappings and transformations
1116
+ # processed_data = process_publishing_data(raw_data, config)
288
1117
  #
289
- # # If custom handler is provided, call it
290
- # if config[:handler]
291
- # Rails.logger.debug "📤 Calling custom publishing handler" if defined?(Rails)
292
- # config[:handler].call(self.class.name, action, processed_data, self)
293
- # else
294
- # # Default publishing behavior - publish the processed data
295
- # config[:subjects].each do |subject|
296
- # NatsWave.client.publish(
297
- # subject: subject,
298
- # model: self.class.name,
299
- # action: action,
300
- # data: processed_data, # This is the processed data, not raw attributes
301
- # metadata: build_publishing_metadata
302
- # )
303
- #
304
- # Rails.logger.info "📤 Published #{self.class.name}##{id} to #{subject} (#{action})" if defined?(Rails)
305
- # end
1118
+ # # Determine which subjects to publish to based on action
1119
+ # subjects_to_publish = determine_subjects_for_action(config[:subjects], action)
1120
+ #
1121
+ # # Always publish first (default behavior)
1122
+ # subjects_to_publish.each do |subject|
1123
+ # NatsWave.client.publish(
1124
+ # subject: subject,
1125
+ # model: self.class.name,
1126
+ # action: action,
1127
+ # data: processed_data, # This is the processed data, not raw attributes
1128
+ # metadata: build_publishing_metadata
1129
+ # )
1130
+ #
1131
+ # Rails.logger.info "📤 Published #{self.class.name}##{id} to #{subject} (#{action})" if defined?(Rails)
1132
+ # end
1133
+ #
1134
+ # # Then call custom handler if provided (optional)
1135
+ # if config[:custom_handler]
1136
+ # Rails.logger.debug "📤 Calling custom publishing handler after publishing" if defined?(Rails)
1137
+ # config[:custom_handler].call(self.class.name, action, processed_data, self)
306
1138
  # end
307
1139
  # end
308
1140
  # rescue StandardError => e
@@ -310,6 +1142,37 @@
310
1142
  # # Don't re-raise to avoid breaking transactions
311
1143
  # end
312
1144
  #
1145
+ # # Auto-publishing callback methods (only called once per action)
1146
+ # def trigger_nats_wave_auto_publish_on_create
1147
+ # return if @nats_wave_publishing_in_progress
1148
+ # @nats_wave_publishing_in_progress = true
1149
+ #
1150
+ # Rails.logger.debug "🚀 Auto-publishing on create" if defined?(Rails)
1151
+ # nats_wave_publish('create')
1152
+ #
1153
+ # @nats_wave_publishing_in_progress = false
1154
+ # end
1155
+ #
1156
+ # def trigger_nats_wave_auto_publish_on_update
1157
+ # return if @nats_wave_publishing_in_progress
1158
+ # @nats_wave_publishing_in_progress = true
1159
+ #
1160
+ # Rails.logger.debug "🚀 Auto-publishing on update" if defined?(Rails)
1161
+ # nats_wave_publish('update')
1162
+ #
1163
+ # @nats_wave_publishing_in_progress = false
1164
+ # end
1165
+ #
1166
+ # def trigger_nats_wave_auto_publish_on_destroy
1167
+ # return if @nats_wave_publishing_in_progress
1168
+ # @nats_wave_publishing_in_progress = true
1169
+ #
1170
+ # Rails.logger.debug "🚀 Auto-publishing on destroy" if defined?(Rails)
1171
+ # nats_wave_publish('destroy')
1172
+ #
1173
+ # @nats_wave_publishing_in_progress = false
1174
+ # end
1175
+ #
313
1176
  # private
314
1177
  #
315
1178
  # def determine_action_from_context
@@ -333,6 +1196,86 @@
333
1196
  # end
334
1197
  # end
335
1198
  #
1199
+ # # Process publishing data through field mappings and transformations (INSTANCE METHOD)
1200
+ # def process_publishing_data(raw_data, config)
1201
+ # processed_data = {}
1202
+ # field_mappings = config[:field_mappings]
1203
+ # transformations = config[:transformations]
1204
+ # skip_fields = config[:skip_fields]
1205
+ # only_mapped_fields = config[:only_mapped_fields]
1206
+ #
1207
+ # # If only_mapped_fields is true, only process fields that have mappings or transformations
1208
+ # fields_to_process = if only_mapped_fields && field_mappings.any?
1209
+ # # Only process fields that are explicitly mapped or have transformations
1210
+ # mapped_source_fields = field_mappings.keys.map(&:to_s)
1211
+ # transformation_fields = transformations.keys.map(&:to_s)
1212
+ # all_relevant_fields = (mapped_source_fields + transformation_fields).uniq
1213
+ #
1214
+ # raw_data.select { |field, _| all_relevant_fields.include?(field.to_s) }
1215
+ # else
1216
+ # # Process all fields (existing behavior)
1217
+ # raw_data
1218
+ # end
1219
+ #
1220
+ # fields_to_process.each do |field, value|
1221
+ # field_str = field.to_s
1222
+ # field_sym = field.to_sym
1223
+ #
1224
+ # # Skip if in skip_fields
1225
+ # next if skip_fields.include?(field_str) || skip_fields.include?(field_sym)
1226
+ #
1227
+ # # Apply field mapping (local -> external)
1228
+ # mapped_field = field_mappings[field_str] ||
1229
+ # field_mappings[field_sym] ||
1230
+ # field
1231
+ #
1232
+ # # Apply transformation if any
1233
+ # transformed_value = apply_publishing_transformation(value, mapped_field, transformations, raw_data)
1234
+ #
1235
+ # processed_data[mapped_field.to_s] = transformed_value
1236
+ # end
1237
+ #
1238
+ # processed_data
1239
+ # end
1240
+ #
1241
+ # def apply_publishing_transformation(value, field, transformations, full_record)
1242
+ # transformation = transformations[field] || transformations[field.to_sym]
1243
+ #
1244
+ # case transformation
1245
+ # when Proc
1246
+ # if transformation.arity == 2 || transformation.arity < 0
1247
+ # transformation.call(value, full_record)
1248
+ # else
1249
+ # transformation.call(value)
1250
+ # end
1251
+ # when Symbol
1252
+ # if self.respond_to?(transformation, true)
1253
+ # self.send(transformation, value)
1254
+ # elsif value.respond_to?(transformation)
1255
+ # value.send(transformation)
1256
+ # else
1257
+ # Rails.logger.warn "Publishing transformation method #{transformation} not found" if defined?(Rails)
1258
+ # value
1259
+ # end
1260
+ # else
1261
+ # value
1262
+ # end
1263
+ # end
1264
+ #
1265
+ # def determine_subjects_for_action(configured_subjects, action)
1266
+ # # If subjects contain placeholders like {action}, replace them
1267
+ # configured_subjects.map do |subject|
1268
+ # if subject.include?('{action}')
1269
+ # subject.gsub('{action}', action.to_s)
1270
+ # elsif subject.include?('*')
1271
+ # # Replace wildcard with specific action
1272
+ # subject.gsub('*', action.to_s)
1273
+ # else
1274
+ # subject
1275
+ # end
1276
+ # end
1277
+ # end
1278
+ #
336
1279
  # def evaluate_conditions(conditions)
337
1280
  # conditions.all? do |condition, expected_value|
338
1281
  # case condition
@@ -361,7 +1304,6 @@
361
1304
  # end
362
1305
  # end
363
1306
  # end
364
-
365
1307
  # frozen_string_literal: true
366
1308
 
367
1309
  module NatsWave
@@ -380,7 +1322,8 @@ module NatsWave
380
1322
  def nats_wave_subscribes_to(*subjects, **options, &block)
381
1323
  Rails.logger.debug "📡 #{self.name}: Setting up subscription to subjects: #{subjects.inspect}" if defined?(Rails)
382
1324
 
383
- handler = options[:handler] || block
1325
+ # Custom handler is now optional and runs AFTER default behavior
1326
+ custom_handler = options[:handler] || block
384
1327
 
385
1328
  subscription_config = {
386
1329
  subjects: subjects.flatten,
@@ -389,7 +1332,7 @@ module NatsWave
389
1332
  skip_fields: options[:skip_fields] || [],
390
1333
  unique_fields: options[:unique_fields] || [:id],
391
1334
  sync_strategy: options[:sync_strategy] || :upsert,
392
- handler: handler,
1335
+ custom_handler: custom_handler, # Renamed to be clear it's optional
393
1336
  queue_group: options[:queue_group],
394
1337
  auto_sync: options[:auto_sync] != false # Default to true, can be disabled
395
1338
  }
@@ -416,7 +1359,8 @@ module NatsWave
416
1359
  def nats_wave_publishes_to(*subjects, **options, &block)
417
1360
  Rails.logger.debug "📤 #{self.name}: Setting up publishing to subjects: #{subjects.inspect}" if defined?(Rails)
418
1361
 
419
- handler = options[:handler] || block
1362
+ # Custom handler is now optional and runs AFTER default behavior
1363
+ custom_handler = options[:handler] || block
420
1364
 
421
1365
  publishing_config = {
422
1366
  subjects: subjects.flatten,
@@ -425,7 +1369,7 @@ module NatsWave
425
1369
  skip_fields: options[:skip_fields] || [],
426
1370
  conditions: options[:conditions] || {},
427
1371
  actions: options[:actions] || [:create, :update, :destroy],
428
- handler: handler,
1372
+ custom_handler: custom_handler, # Renamed to be clear it's optional
429
1373
  async: options[:async] != false, # Default to true
430
1374
  enabled: options[:enabled] != false # Default to true
431
1375
  }
@@ -434,8 +1378,8 @@ module NatsWave
434
1378
  config_key = subjects.join(',')
435
1379
  self.nats_wave_publishing_config[config_key] = publishing_config
436
1380
 
437
- # Add ActiveRecord callbacks for auto-publishing
438
- setup_publishing_callbacks(publishing_config)
1381
+ # Add ActiveRecord callbacks for auto-publishing (only once per model)
1382
+ setup_publishing_callbacks_once
439
1383
 
440
1384
  Rails.logger.debug "📤 #{self.name}: Registered publishing config for subjects: #{subjects}" if defined?(Rails)
441
1385
  end
@@ -450,39 +1394,23 @@ module NatsWave
450
1394
  nats_wave_publishing_config
451
1395
  end
452
1396
 
453
- # Get all subjects this model subscribes to
454
- def nats_wave_subscribed_subjects
455
- nats_wave_subscription_config.values.flat_map { |config| config[:subjects] }.uniq
456
- end
457
-
458
- # Get all subjects this model publishes to
459
- def nats_wave_published_subjects
460
- nats_wave_publishing_config.values.flat_map { |config| config[:subjects] }.uniq
461
- end
462
-
463
1397
  private
464
1398
 
465
- # Setup ActiveRecord callbacks for auto-publishing
466
- def setup_publishing_callbacks(config)
467
- # Only add callbacks if we're in ActiveRecord and actions are specified
1399
+ # Setup ActiveRecord callbacks for auto-publishing (only once per model)
1400
+ def setup_publishing_callbacks_once
1401
+ # Only add callbacks if we're in ActiveRecord and haven't added them yet
468
1402
  return unless defined?(ActiveRecord::Base) && self < ActiveRecord::Base
1403
+ return if @nats_wave_callbacks_added
469
1404
 
470
- actions = config[:actions]
1405
+ after_commit :trigger_nats_wave_auto_publish_on_create, on: :create
1406
+ after_commit :trigger_nats_wave_auto_publish_on_update, on: :update
1407
+ after_commit :trigger_nats_wave_auto_publish_on_destroy, on: :destroy
471
1408
 
472
- if actions.include?(:create)
473
- after_commit :trigger_nats_wave_auto_publish_on_create, on: :create
474
- end
475
-
476
- if actions.include?(:update)
477
- after_commit :trigger_nats_wave_auto_publish_on_update, on: :update
478
- end
479
-
480
- if actions.include?(:destroy)
481
- after_commit :trigger_nats_wave_auto_publish_on_destroy, on: :destroy
482
- end
1409
+ @nats_wave_callbacks_added = true
1410
+ Rails.logger.debug "📤 #{self.name}: Added publishing callbacks" if defined?(Rails)
483
1411
  end
484
1412
 
485
- # Create a subscription handler that processes data and calls custom handler or auto-sync
1413
+ # Create a subscription handler that processes data and calls custom handler AFTER auto-sync
486
1414
  def create_subscription_handler(config)
487
1415
  model_class = self
488
1416
 
@@ -496,18 +1424,18 @@ module NatsWave
496
1424
  action = message['action']
497
1425
 
498
1426
  # Process the data through mappings and transformations
499
- processed_data = process_subscription_data(raw_data, config)
1427
+ processed_data = model_class.process_subscription_data(raw_data, config)
500
1428
 
501
- # If custom handler is provided, call it with model_name, action, processed_data
502
- if config[:handler]
503
- Rails.logger.debug "📨 Calling custom subscription handler" if defined?(Rails)
504
- config[:handler].call(model_name, action, processed_data, message)
505
- elsif config[:auto_sync]
506
- Rails.logger.debug "📨 Performing auto-sync" if defined?(Rails)
507
- # Default auto-sync behavior
1429
+ # Always perform auto-sync first (if enabled)
1430
+ if config[:auto_sync]
1431
+ Rails.logger.debug "📨 Performing auto-sync first" if defined?(Rails)
508
1432
  model_class.perform_auto_sync(model_name, action, processed_data, config)
509
- else
510
- Rails.logger.debug "📨 No handler provided and auto_sync disabled" if defined?(Rails)
1433
+ end
1434
+
1435
+ # Then call custom handler if provided (optional)
1436
+ if config[:custom_handler]
1437
+ Rails.logger.debug "📨 Calling custom subscription handler after auto-sync" if defined?(Rails)
1438
+ config[:custom_handler].call(model_name, action, processed_data, message)
511
1439
  end
512
1440
 
513
1441
  rescue => e
@@ -518,7 +1446,7 @@ module NatsWave
518
1446
  end
519
1447
  end
520
1448
 
521
- # Process subscription data through field mappings and transformations (CLASS METHOD)
1449
+ # Process subscription data through field mappings and transformations (PUBLIC CLASS METHOD)
522
1450
  def process_subscription_data(raw_data, config)
523
1451
  processed_data = {}
524
1452
  field_mappings = config[:field_mappings]
@@ -532,7 +1460,7 @@ module NatsWave
532
1460
  # Skip if in skip_fields
533
1461
  next if skip_fields.include?(field_str) || skip_fields.include?(field_sym)
534
1462
 
535
- # Apply field mapping
1463
+ # Apply field mapping (external -> local)
536
1464
  mapped_field = field_mappings[field_str] ||
537
1465
  field_mappings[field_sym] ||
538
1466
  field
@@ -546,7 +1474,7 @@ module NatsWave
546
1474
  processed_data
547
1475
  end
548
1476
 
549
- # Apply transformations to field values (CLASS METHOD)
1477
+ # Apply transformations to field values (PUBLIC CLASS METHOD)
550
1478
  def apply_transformation(value, field, transformations, full_record)
551
1479
  transformation = transformations[field] || transformations[field.to_sym]
552
1480
 
@@ -571,7 +1499,7 @@ module NatsWave
571
1499
  end
572
1500
  end
573
1501
 
574
- # Default auto-sync behavior (CLASS METHOD)
1502
+ # Default auto-sync behavior (PUBLIC CLASS METHOD)
575
1503
  def perform_auto_sync(model_name, action, processed_data, config)
576
1504
  unique_fields = config[:unique_fields]
577
1505
  sync_strategy = config[:sync_strategy]
@@ -673,23 +1601,26 @@ module NatsWave
673
1601
  # Process the data through mappings and transformations
674
1602
  processed_data = process_publishing_data(raw_data, config)
675
1603
 
676
- # If custom handler is provided, call it
677
- if config[:handler]
678
- Rails.logger.debug "📤 Calling custom publishing handler" if defined?(Rails)
679
- config[:handler].call(self.class.name, action, processed_data, self)
680
- else
681
- # Default publishing behavior - publish the processed data
682
- config[:subjects].each do |subject|
683
- NatsWave.client.publish(
684
- subject: subject,
685
- model: self.class.name,
686
- action: action,
687
- data: processed_data, # This is the processed data, not raw attributes
688
- metadata: build_publishing_metadata
689
- )
690
-
691
- Rails.logger.info "📤 Published #{self.class.name}##{id} to #{subject} (#{action})" if defined?(Rails)
692
- end
1604
+ # Determine which subjects to publish to based on action
1605
+ subjects_to_publish = determine_subjects_for_action(config[:subjects], action)
1606
+
1607
+ # Always publish first (default behavior)
1608
+ subjects_to_publish.each do |subject|
1609
+ NatsWave.client.publish(
1610
+ subject: subject,
1611
+ model: self.class.name,
1612
+ action: action,
1613
+ data: processed_data, # This is the processed data, not raw attributes
1614
+ metadata: build_publishing_metadata
1615
+ )
1616
+
1617
+ Rails.logger.info "📤 Published #{self.class.name}##{id} to #{subject} (#{action})" if defined?(Rails)
1618
+ end
1619
+
1620
+ # Then call custom handler if provided (optional)
1621
+ if config[:custom_handler]
1622
+ Rails.logger.debug "📤 Calling custom publishing handler after publishing" if defined?(Rails)
1623
+ config[:custom_handler].call(self.class.name, action, processed_data, self)
693
1624
  end
694
1625
  end
695
1626
  rescue StandardError => e
@@ -697,20 +1628,35 @@ module NatsWave
697
1628
  # Don't re-raise to avoid breaking transactions
698
1629
  end
699
1630
 
700
- # Auto-publishing callback methods
1631
+ # Auto-publishing callback methods (only called once per action)
701
1632
  def trigger_nats_wave_auto_publish_on_create
1633
+ return if @nats_wave_publishing_in_progress
1634
+ @nats_wave_publishing_in_progress = true
1635
+
702
1636
  Rails.logger.debug "🚀 Auto-publishing on create" if defined?(Rails)
703
1637
  nats_wave_publish('create')
1638
+
1639
+ @nats_wave_publishing_in_progress = false
704
1640
  end
705
1641
 
706
1642
  def trigger_nats_wave_auto_publish_on_update
1643
+ return if @nats_wave_publishing_in_progress
1644
+ @nats_wave_publishing_in_progress = true
1645
+
707
1646
  Rails.logger.debug "🚀 Auto-publishing on update" if defined?(Rails)
708
1647
  nats_wave_publish('update')
1648
+
1649
+ @nats_wave_publishing_in_progress = false
709
1650
  end
710
1651
 
711
1652
  def trigger_nats_wave_auto_publish_on_destroy
1653
+ return if @nats_wave_publishing_in_progress
1654
+ @nats_wave_publishing_in_progress = true
1655
+
712
1656
  Rails.logger.debug "🚀 Auto-publishing on destroy" if defined?(Rails)
713
1657
  nats_wave_publish('destroy')
1658
+
1659
+ @nats_wave_publishing_in_progress = false
714
1660
  end
715
1661
 
716
1662
  private
@@ -736,7 +1682,7 @@ module NatsWave
736
1682
  end
737
1683
  end
738
1684
 
739
- # Process publishing data through field mappings and transformations (INSTANCE METHOD)
1685
+ # Process publishing data - much simpler now! (INSTANCE METHOD)
740
1686
  def process_publishing_data(raw_data, config)
741
1687
  processed_data = {}
742
1688
  field_mappings = config[:field_mappings]
@@ -750,10 +1696,10 @@ module NatsWave
750
1696
  # Skip if in skip_fields
751
1697
  next if skip_fields.include?(field_str) || skip_fields.include?(field_sym)
752
1698
 
753
- # Apply field mapping (local -> external)
1699
+ # Apply field mapping (local -> external) - ONLY if mapping exists
754
1700
  mapped_field = field_mappings[field_str] ||
755
1701
  field_mappings[field_sym] ||
756
- field
1702
+ field # Use original field name if no mapping
757
1703
 
758
1704
  # Apply transformation if any
759
1705
  transformed_value = apply_publishing_transformation(value, mapped_field, transformations, raw_data)
@@ -788,6 +1734,20 @@ module NatsWave
788
1734
  end
789
1735
  end
790
1736
 
1737
+ def determine_subjects_for_action(configured_subjects, action)
1738
+ # If subjects contain placeholders like {action}, replace them
1739
+ configured_subjects.map do |subject|
1740
+ if subject.include?('{action}')
1741
+ subject.gsub('{action}', action.to_s)
1742
+ elsif subject.include?('*')
1743
+ # Replace wildcard with specific action
1744
+ subject.gsub('*', action.to_s)
1745
+ else
1746
+ subject
1747
+ end
1748
+ end
1749
+ end
1750
+
791
1751
  def evaluate_conditions(conditions)
792
1752
  conditions.all? do |condition, expected_value|
793
1753
  case condition