nats_wave 1.1.14 → 1.1.15

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7355520886942caba83b3eb8b9bc59e086639036d403b72011f153e2eb626020
4
- data.tar.gz: 7c1fe995248ee6c591efecb6725d25c562d6b4b61e897a319383c284e11dd919
3
+ metadata.gz: 4777c9a4a662a49320023b9a7d990f85048104bb9aa6a69e7560c16032d93e1d
4
+ data.tar.gz: 87f7b15fa4fad543c57f79724179559921da10a86782f2bc8beb7ad9ca06a316
5
5
  SHA512:
6
- metadata.gz: e2817cea70331047fb86a41d952b889afb20a6525a3b3cb581af60d12d1d03b28d7d4628b5aca11de313e9c222b0e3e1ecfbb138a5281ba6e3878973c948573d
7
- data.tar.gz: 3b75a4d981dda6c4744ef16eb6a4f6eb55bee2c27bf925f2fc44f6a6a28310587ecf4385d47e90a4ed127890f267eb9be552f5e0a9011cc93648fa86702f38c8
6
+ metadata.gz: ea31a18412ee4bc53dc81b5847daf5387419d5c2e3deeab2f5b704679533a53125a5dda6ed341521aa1dfc8c5e0aacfdd740df329250750b7b8d61e9c1f79053
7
+ data.tar.gz: a96f7870c5e44c9c73da2b7c807e30a130d672c84c55c9d7ab718f5db75809c1f3fcc4c31190e7a6b33be86b779c02099bc63aff93a4f0bfbbc8c24f7ce84e1b
data/.idea/nats_wave.iml CHANGED
@@ -129,21 +129,21 @@
129
129
  <option name="myRootTask">
130
130
  <RakeTaskImpl id="rake">
131
131
  <subtasks>
132
- <RakeTaskImpl description="Build nats_wave-1.1.6.gem into the pkg directory" fullCommand="build" id="build" />
132
+ <RakeTaskImpl description="Build nats_wave-1.1.14.gem into the pkg directory" fullCommand="build" id="build" />
133
133
  <RakeTaskImpl id="build">
134
134
  <subtasks>
135
- <RakeTaskImpl description="Generate SHA512 checksum of nats_wave-1.1.6.gem into the checksums directory" fullCommand="build:checksum" id="checksum" />
135
+ <RakeTaskImpl description="Generate SHA512 checksum of nats_wave-1.1.14.gem into the checksums directory" fullCommand="build:checksum" id="checksum" />
136
136
  </subtasks>
137
137
  </RakeTaskImpl>
138
138
  <RakeTaskImpl description="Remove any temporary products" fullCommand="clean" id="clean" />
139
139
  <RakeTaskImpl description="Remove any generated files" fullCommand="clobber" id="clobber" />
140
- <RakeTaskImpl description="Build and install nats_wave-1.1.6.gem into system gems" fullCommand="install" id="install" />
140
+ <RakeTaskImpl description="Build and install nats_wave-1.1.14.gem into system gems" fullCommand="install" id="install" />
141
141
  <RakeTaskImpl id="install">
142
142
  <subtasks>
143
- <RakeTaskImpl description="Build and install nats_wave-1.1.6.gem into system gems without network access" fullCommand="install:local" id="local" />
143
+ <RakeTaskImpl description="Build and install nats_wave-1.1.14.gem into system gems without network access" fullCommand="install:local" id="local" />
144
144
  </subtasks>
145
145
  </RakeTaskImpl>
146
- <RakeTaskImpl description="Create tag v1.1.6 and build and push nats_wave-1.1.6.gem to https://rubygems.org" fullCommand="release[remote]" id="release[remote]" />
146
+ <RakeTaskImpl description="Create tag v1.1.14 and build and push nats_wave-1.1.14.gem to https://rubygems.org" fullCommand="release[remote]" id="release[remote]" />
147
147
  <RakeTaskImpl description="Run RuboCop" fullCommand="rubocop" id="rubocop" />
148
148
  <RakeTaskImpl id="rubocop">
149
149
  <subtasks>
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- nats_wave (1.1.14)
4
+ nats_wave (1.1.15)
5
5
  activerecord (>= 6.1, < 8.0)
6
6
  activesupport (>= 6.1, < 8.0)
7
7
  concurrent-ruby (~> 1.1)
@@ -77,7 +77,7 @@ module NatsWave
77
77
  {
78
78
  service: @service_name,
79
79
  environment: ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'unknown',
80
- version: ENV['APP_VERSION'] || '1.1.14'
80
+ version: ENV['APP_VERSION'] || '1.1.15'
81
81
  }
82
82
  end
83
83
 
@@ -1,3 +1,989 @@
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
+ # # NatsWave.logger.debug "📡 #{self.name}: Setting up subscription to subjects: #{subjects.inspect}"
18
+ # #
19
+ # # # Custom handler is now optional and runs AFTER default behavior
20
+ # # custom_handler = options[:handler] || block
21
+ # #
22
+ # # subscription_config = {
23
+ # # subjects: subjects.flatten,
24
+ # # field_mappings: options[:field_mappings] || {},
25
+ # # transformations: options[:transformations] || {},
26
+ # # skip_fields: options[:skip_fields] || [],
27
+ # # unique_fields: options[:unique_fields] || [:id],
28
+ # # sync_strategy: options[:sync_strategy] || :upsert,
29
+ # # custom_handler: custom_handler, # Renamed to be clear it's optional
30
+ # # queue_group: options[:queue_group],
31
+ # # auto_sync: options[:auto_sync] != false # Default to true, can be disabled
32
+ # # }
33
+ # #
34
+ # # # Store the config
35
+ # # config_key = subjects.join(',')
36
+ # # self.nats_wave_subscription_config[config_key] = subscription_config
37
+ # #
38
+ # # # Create the subscription handler
39
+ # # processed_handler = create_subscription_handler(subscription_config)
40
+ # #
41
+ # # # Register the subscription
42
+ # # NatsWave::ModelRegistry.register_subscription(
43
+ # # subjects: subjects.flatten,
44
+ # # model: self.name,
45
+ # # handler: processed_handler,
46
+ # # queue_group: subscription_config[:queue_group]
47
+ # # )
48
+ # #
49
+ # # NatsWave.logger.debug "📡 #{self.name}: Registered subscription for subjects: #{subjects}"
50
+ # # end
51
+ # #
52
+ # # # Configure publishing with optional custom handler AND auto-publishing callbacks
53
+ # # def nats_wave_publishes_to(*subjects, **options, &block)
54
+ # # NatsWave.logger.debug "📤 #{self.name}: Setting up publishing to subjects: #{subjects.inspect}"
55
+ # #
56
+ # # # Custom handler is now optional and runs AFTER default behavior
57
+ # # custom_handler = options[:handler] || block
58
+ # #
59
+ # # publishing_config = {
60
+ # # subjects: subjects.flatten,
61
+ # # field_mappings: options[:field_mappings] || {},
62
+ # # transformations: options[:transformations] || {},
63
+ # # skip_fields: options[:skip_fields] || [],
64
+ # # conditions: options[:conditions] || {},
65
+ # # actions: options[:actions] || [:create, :update, :destroy],
66
+ # # custom_handler: custom_handler, # Renamed to be clear it's optional
67
+ # # async: options[:async] != false, # Default to true
68
+ # # enabled: options[:enabled] != false, # Default to true
69
+ # # only_mapped_fields: options[:only_mapped_fields] != false, # Default to true
70
+ # # }
71
+ # #
72
+ # # # Store the config
73
+ # # config_key = subjects.join(',')
74
+ # # self.nats_wave_publishing_config[config_key] = publishing_config
75
+ # #
76
+ # # # Add ActiveRecord callbacks for auto-publishing (only once per model)
77
+ # # setup_publishing_callbacks_once
78
+ # #
79
+ # # NatsWave.logger.debug "📤 #{self.name}: Registered publishing config for subjects: #{subjects}"
80
+ # # end
81
+ # #
82
+ # # # Get all subscription configurations
83
+ # # def nats_wave_subscription_configs
84
+ # # nats_wave_subscription_config
85
+ # # end
86
+ # #
87
+ # # # Get all publishing configurations
88
+ # # def nats_wave_publishing_configs
89
+ # # nats_wave_publishing_config
90
+ # # end
91
+ # #
92
+ # # private
93
+ # #
94
+ # # # Setup ActiveRecord callbacks for auto-publishing (only once per model)
95
+ # # def setup_publishing_callbacks_once
96
+ # # # Only add callbacks if we're in ActiveRecord and haven't added them yet
97
+ # # return unless defined?(ActiveRecord::Base) && self < ActiveRecord::Base
98
+ # # return if @nats_wave_callbacks_added
99
+ # #
100
+ # # after_commit :trigger_nats_wave_auto_publish_on_create, on: :create
101
+ # # after_commit :trigger_nats_wave_auto_publish_on_update, on: :update
102
+ # # after_commit :trigger_nats_wave_auto_publish_on_destroy, on: :destroy
103
+ # #
104
+ # # @nats_wave_callbacks_added = true
105
+ # # NatsWave.logger.debug "📤 #{self.name}: Added publishing callbacks"
106
+ # # end
107
+ # #
108
+ # # # Create a subscription handler that processes data and calls custom handler AFTER auto-sync
109
+ # # def create_subscription_handler(config)
110
+ # # model_class = self
111
+ # #
112
+ # # lambda do |message|
113
+ # # begin
114
+ # # NatsWave.logger.debug "📨 Processing subscription message for #{model_class.name}"
115
+ # #
116
+ # # # Extract the raw data
117
+ # # raw_data = message['data'] || {}
118
+ # # model_name = message['model']
119
+ # # action = message['action']
120
+ # #
121
+ # # # Process the data through mappings and transformations
122
+ # # processed_data = model_class.process_subscription_data(raw_data, config)
123
+ # #
124
+ # # # Always perform auto-sync first (if enabled)
125
+ # # if config[:auto_sync]
126
+ # # NatsWave.logger.debug "📨 Performing auto-sync first"
127
+ # # model_class.perform_auto_sync(model_name, action, processed_data, config)
128
+ # # end
129
+ # #
130
+ # # # Then call custom handler if provided (optional)
131
+ # # if config[:custom_handler]
132
+ # # NatsWave.logger.debug "📨 Calling custom subscription handler after auto-sync"
133
+ # # config[:custom_handler].call(model_name, action, processed_data, message)
134
+ # # end
135
+ # #
136
+ # # rescue => e
137
+ # # NatsWave.logger.error "Error in subscription handler for #{model_class.name}: #{e.message}"
138
+ # # NatsWave.logger.error e.backtrace.join("\n")
139
+ # # raise
140
+ # # end
141
+ # # end
142
+ # # end
143
+ # #
144
+ # # # Process subscription data through field mappings and transformations (PUBLIC CLASS METHOD)
145
+ # # def process_subscription_data(raw_data, config)
146
+ # # processed_data = {}
147
+ # # field_mappings = config[:field_mappings]
148
+ # # transformations = config[:transformations]
149
+ # # skip_fields = config[:skip_fields]
150
+ # #
151
+ # # raw_data.each do |field, value|
152
+ # # field_str = field.to_s
153
+ # # field_sym = field.to_sym
154
+ # #
155
+ # # # Skip if in skip_fields
156
+ # # next if skip_fields.include?(field_str) || skip_fields.include?(field_sym)
157
+ # #
158
+ # # # Apply field mapping (external -> local)
159
+ # # mapped_field = field_mappings[field_str] ||
160
+ # # field_mappings[field_sym] ||
161
+ # # field
162
+ # #
163
+ # # # Apply transformation if any
164
+ # # transformed_value = apply_transformation(value, mapped_field, transformations, raw_data)
165
+ # #
166
+ # # processed_data[mapped_field.to_s] = transformed_value
167
+ # # end
168
+ # #
169
+ # # processed_data
170
+ # # end
171
+ # #
172
+ # # # Apply transformations to field values (PUBLIC CLASS METHOD)
173
+ # # def apply_transformation(value, field, transformations, full_record)
174
+ # # transformation = transformations[field] || transformations[field.to_sym]
175
+ # #
176
+ # # case transformation
177
+ # # when Proc
178
+ # # if transformation.arity == 2 || transformation.arity < 0
179
+ # # transformation.call(value, full_record)
180
+ # # else
181
+ # # transformation.call(value)
182
+ # # end
183
+ # # when Symbol
184
+ # # if self.respond_to?(transformation, true)
185
+ # # self.send(transformation, value)
186
+ # # elsif value.respond_to?(transformation)
187
+ # # value.send(transformation)
188
+ # # else
189
+ # # NatsWave.logger.warn "Transformation method #{transformation} not found"
190
+ # # value
191
+ # # end
192
+ # # else
193
+ # # value
194
+ # # end
195
+ # # end
196
+ # #
197
+ # # # Default auto-sync behavior (PUBLIC CLASS METHOD)
198
+ # # def perform_auto_sync(model_name, action, processed_data, config)
199
+ # # unique_fields = config[:unique_fields]
200
+ # # sync_strategy = config[:sync_strategy]
201
+ # #
202
+ # # case action.to_s.downcase
203
+ # # when 'create', 'created'
204
+ # # handle_auto_create(processed_data, unique_fields, sync_strategy)
205
+ # # when 'update', 'updated'
206
+ # # handle_auto_update(processed_data, unique_fields, sync_strategy)
207
+ # # when 'delete', 'deleted', 'destroy', 'destroyed'
208
+ # # handle_auto_delete(processed_data, unique_fields)
209
+ # # else
210
+ # # NatsWave.logger.warn "Unknown action for auto-sync: #{action}"
211
+ # # end
212
+ # # end
213
+ # #
214
+ # # def handle_auto_create(data, unique_fields, sync_strategy)
215
+ # # case sync_strategy
216
+ # # when :upsert
217
+ # # existing = find_by_unique_fields(data, unique_fields)
218
+ # # if existing
219
+ # # existing.update!(data)
220
+ # # NatsWave.logger.info "✅ Updated existing #{self.name}: #{existing.id}"
221
+ # # else
222
+ # # record = self.create!(data)
223
+ # # NatsWave.logger.info "✅ Created new #{self.name}: #{record.id}"
224
+ # # end
225
+ # # when :create_only
226
+ # # record = self.create!(data)
227
+ # # NatsWave.logger.info "✅ Created new #{self.name}: #{record.id}"
228
+ # # end
229
+ # # rescue => e
230
+ # # NatsWave.logger.error "❌ Failed to create #{self.name}: #{e.message}"
231
+ # # raise
232
+ # # end
233
+ # #
234
+ # # def handle_auto_update(data, unique_fields, sync_strategy)
235
+ # # existing = find_by_unique_fields(data, unique_fields)
236
+ # # if existing
237
+ # # existing.update!(data)
238
+ # # NatsWave.logger.info "✅ Updated #{self.name}: #{existing.id}"
239
+ # # elsif sync_strategy == :upsert
240
+ # # record = self.create!(data)
241
+ # # NatsWave.logger.info "✅ Created new #{self.name} during update: #{record.id}"
242
+ # # else
243
+ # # NatsWave.logger.warn "⚠️ Record not found for update: #{data.slice(*unique_fields.map(&:to_s))}"
244
+ # # end
245
+ # # rescue => e
246
+ # # NatsWave.logger.error "❌ Failed to update #{self.name}: #{e.message}"
247
+ # # raise
248
+ # # end
249
+ # #
250
+ # # def handle_auto_delete(data, unique_fields)
251
+ # # existing = find_by_unique_fields(data, unique_fields)
252
+ # # if existing
253
+ # # existing.destroy!
254
+ # # NatsWave.logger.info "✅ Deleted #{self.name}: #{existing.id}"
255
+ # # else
256
+ # # NatsWave.logger.warn "⚠️ Record not found for deletion: #{data.slice(*unique_fields.map(&:to_s))}"
257
+ # # end
258
+ # # rescue => e
259
+ # # NatsWave.logger.error "❌ Failed to delete #{self.name}: #{e.message}"
260
+ # # raise
261
+ # # end
262
+ # #
263
+ # # def find_by_unique_fields(data, unique_fields)
264
+ # # conditions = {}
265
+ # # unique_fields.each do |field|
266
+ # # field_str = field.to_s
267
+ # # if data.key?(field_str)
268
+ # # conditions[field] = data[field_str]
269
+ # # elsif data.key?(field.to_sym)
270
+ # # conditions[field] = data[field.to_sym]
271
+ # # end
272
+ # # end
273
+ # #
274
+ # # return nil if conditions.empty?
275
+ # # self.find_by(conditions)
276
+ # # end
277
+ # # end
278
+ # #
279
+ # # # Instance methods for publishing
280
+ # # def nats_wave_publish(action = nil)
281
+ # # action ||= determine_action_from_context
282
+ # #
283
+ # # self.class.nats_wave_publishing_configs.each do |subjects_key, config|
284
+ # # next unless config[:enabled]
285
+ # # next unless config[:actions].include?(action.to_sym)
286
+ # #
287
+ # # # Check conditions (only for publishing)
288
+ # # if config[:conditions].any? && !evaluate_conditions(config[:conditions])
289
+ # # NatsWave.logger.debug "📤 Skipping publish - conditions not met"
290
+ # # next
291
+ # # end
292
+ # #
293
+ # # # Get the raw data (current model attributes)
294
+ # # raw_data = get_raw_attributes
295
+ # #
296
+ # # # Process the data through mappings and transformations
297
+ # # processed_data = process_publishing_data(raw_data, config)
298
+ # #
299
+ # # # Determine which subjects to publish to based on action
300
+ # # subjects_to_publish = determine_subjects_for_action(config[:subjects], action)
301
+ # #
302
+ # # # Always publish first (default behavior)
303
+ # # subjects_to_publish.each do |subject|
304
+ # # NatsWave.client.publish(
305
+ # # subject: subject,
306
+ # # model: self.class.name,
307
+ # # action: action,
308
+ # # data: processed_data, # This is the processed data, not raw attributes
309
+ # # metadata: build_publishing_metadata
310
+ # # )
311
+ # #
312
+ # # NatsWave.logger.info "📤 Published #{self.class.name} to #{subject}"
313
+ # # end
314
+ # #
315
+ # # # Then call custom handler if provided (optional)
316
+ # # if config[:custom_handler]
317
+ # # NatsWave.logger.debug "📤 Calling custom publishing handler after publishing"
318
+ # # config[:custom_handler].call(self.class.name, action, processed_data, self)
319
+ # # end
320
+ # # end
321
+ # # rescue StandardError => e
322
+ # # NatsWave.logger.error("Failed to publish: #{e.message}")
323
+ # # # Don't re-raise to avoid breaking transactions
324
+ # # end
325
+ # #
326
+ # # # Auto-publishing callback methods (only called once per action)
327
+ # # def trigger_nats_wave_auto_publish_on_create
328
+ # # return if @nats_wave_publishing_in_progress
329
+ # # @nats_wave_publishing_in_progress = true
330
+ # #
331
+ # # NatsWave.logger.debug "🚀 Auto-publishing on create"
332
+ # # nats_wave_publish('create')
333
+ # #
334
+ # # @nats_wave_publishing_in_progress = false
335
+ # # end
336
+ # #
337
+ # # def trigger_nats_wave_auto_publish_on_update
338
+ # # return if @nats_wave_publishing_in_progress
339
+ # # @nats_wave_publishing_in_progress = true
340
+ # #
341
+ # # NatsWave.logger.debug "🚀 Auto-publishing on update"
342
+ # # nats_wave_publish('update')
343
+ # #
344
+ # # @nats_wave_publishing_in_progress = false
345
+ # # end
346
+ # #
347
+ # # def trigger_nats_wave_auto_publish_on_destroy
348
+ # # return if @nats_wave_publishing_in_progress
349
+ # # @nats_wave_publishing_in_progress = true
350
+ # #
351
+ # # NatsWave.logger.debug "🚀 Auto-publishing on destroy"
352
+ # # nats_wave_publish('destroy')
353
+ # #
354
+ # # @nats_wave_publishing_in_progress = false
355
+ # # end
356
+ # #
357
+ # # private
358
+ # #
359
+ # # def determine_action_from_context
360
+ # # # Try to determine action from ActiveRecord context
361
+ # # if defined?(ActiveRecord) && is_a?(ActiveRecord::Base)
362
+ # # return 'create' if previously_new_record?
363
+ # # return 'update' if saved_changes.any?
364
+ # # return 'destroy' if destroyed?
365
+ # # end
366
+ # #
367
+ # # 'update' # Default fallback
368
+ # # end
369
+ # #
370
+ # # def get_raw_attributes
371
+ # # # Get attributes (works for both ActiveRecord and other objects)
372
+ # # if respond_to?(:attributes)
373
+ # # attributes
374
+ # # else
375
+ # # # For non-ActiveRecord objects, get instance variables
376
+ # # instance_variables.map { |v| [v.to_s.delete('@'), instance_variable_get(v)] }.to_h
377
+ # # end
378
+ # # end
379
+ # #
380
+ # # def process_publishing_data(raw_data, config)
381
+ # # processed_data = {}
382
+ # # field_mappings = config[:field_mappings] || {}
383
+ # # transformations = config[:transformations] || {}
384
+ # # skip_fields = config[:skip_fields] || []
385
+ # # only_mapped_fields = config[:only_mapped_fields]
386
+ # #
387
+ # # # If only_mapped_fields is true, only process fields that have explicit mappings
388
+ # # if only_mapped_fields && field_mappings.any?
389
+ # # field_mappings.each do |local_field, external_field|
390
+ # # local_field_str = local_field.to_s
391
+ # #
392
+ # # # Skip if this field is in skip_fields
393
+ # # next if skip_fields.include?(local_field_str) || skip_fields.include?(local_field.to_sym)
394
+ # #
395
+ # # # Get the value from raw_data
396
+ # # value = raw_data[local_field_str] || raw_data[local_field.to_sym]
397
+ # #
398
+ # # # Apply transformation if any (use external field name for transformation lookup)
399
+ # # transformed_value = apply_publishing_transformation(value, external_field, transformations, raw_data)
400
+ # #
401
+ # # processed_data[external_field.to_s] = transformed_value
402
+ # # end
403
+ # # else
404
+ # # # Original behavior - process all fields
405
+ # # raw_data.each do |field, value|
406
+ # # field_str = field.to_s
407
+ # # field_sym = field.to_sym
408
+ # #
409
+ # # # Skip if in skip_fields
410
+ # # next if skip_fields.include?(field_str) || skip_fields.include?(field_sym)
411
+ # #
412
+ # # # Apply field mapping (local -> external) - ONLY if mapping exists
413
+ # # mapped_field = field_mappings[field_str] ||
414
+ # # field_mappings[field_sym] ||
415
+ # # field # Use original field name if no mapping
416
+ # #
417
+ # # # Apply transformation if any
418
+ # # transformed_value = apply_publishing_transformation(value, mapped_field, transformations, raw_data)
419
+ # #
420
+ # # processed_data[mapped_field.to_s] = transformed_value
421
+ # # end
422
+ # # end
423
+ # # processed_data
424
+ # # end
425
+ # #
426
+ # # def apply_publishing_transformation(value, field, transformations, full_record)
427
+ # # transformation = transformations[field] || transformations[field.to_sym]
428
+ # #
429
+ # # case transformation
430
+ # # when Proc
431
+ # # if transformation.arity == 2 || transformation.arity < 0
432
+ # # transformation.call(value, full_record)
433
+ # # else
434
+ # # transformation.call(value)
435
+ # # end
436
+ # # when Symbol
437
+ # # if self.respond_to?(transformation, true)
438
+ # # self.send(transformation, value)
439
+ # # elsif value.respond_to?(transformation)
440
+ # # value.send(transformation)
441
+ # # else
442
+ # # NatsWave.logger.warn "Publishing transformation method #{transformation} not found"
443
+ # # value
444
+ # # end
445
+ # # else
446
+ # # value
447
+ # # end
448
+ # # end
449
+ # #
450
+ # # def determine_subjects_for_action(configured_subjects, action)
451
+ # # # If subjects contain placeholders like {action}, replace them
452
+ # # configured_subjects.map do |subject|
453
+ # # if subject.include?('{action}')
454
+ # # subject.gsub('{action}', action.to_s)
455
+ # # elsif subject.include?('*')
456
+ # # # Replace wildcard with specific action
457
+ # # subject.gsub('*', action.to_s)
458
+ # # else
459
+ # # subject
460
+ # # end
461
+ # # end
462
+ # # end
463
+ # #
464
+ # # def evaluate_conditions(conditions)
465
+ # # conditions.all? do |condition, expected_value|
466
+ # # case condition
467
+ # # when Proc
468
+ # # condition.call(self)
469
+ # # when Symbol, String
470
+ # # if respond_to?(condition, true)
471
+ # # result = send(condition)
472
+ # # expected_value.nil? ? result : result == expected_value
473
+ # # else
474
+ # # false
475
+ # # end
476
+ # # else
477
+ # # false
478
+ # # end
479
+ # # end
480
+ # # end
481
+ # #
482
+ # # def build_publishing_metadata
483
+ # # {
484
+ # # source_model: self.class.name,
485
+ # # source_id: respond_to?(:id) ? id : object_id,
486
+ # # published_at: Time.current.iso8601
487
+ # # }
488
+ # # end
489
+ # # end
490
+ # # end
491
+ # # end
492
+ #
493
+ # # frozen_string_literal: true
494
+ #
495
+ # module NatsWave
496
+ # module Concerns
497
+ # module Mappable
498
+ # extend ActiveSupport::Concern
499
+ #
500
+ # included do
501
+ # class_attribute :nats_wave_subscription_config, :nats_wave_publishing_config
502
+ # self.nats_wave_subscription_config = {}
503
+ # self.nats_wave_publishing_config = {}
504
+ # end
505
+ #
506
+ # class_methods do
507
+ # # Configure subscriptions with optional custom handler
508
+ # def nats_wave_subscribes_to(*subjects, **options, &block)
509
+ # NatsWave.logger.debug "📡 #{self.name}: Setting up subscription to subjects: #{subjects.inspect}"
510
+ #
511
+ # # Custom handler is now optional and runs AFTER default behavior
512
+ # custom_handler = options[:handler] || block
513
+ #
514
+ # subscription_config = {
515
+ # subjects: subjects.flatten,
516
+ # field_mappings: options[:field_mappings] || {},
517
+ # transformations: options[:transformations] || {},
518
+ # skip_fields: options[:skip_fields] || [],
519
+ # unique_fields: options[:unique_fields] || [:id],
520
+ # sync_strategy: options[:sync_strategy] || :upsert,
521
+ # custom_handler: custom_handler, # Renamed to be clear it's optional
522
+ # queue_group: options[:queue_group],
523
+ # auto_sync: options[:auto_sync] != false # Default to true, can be disabled
524
+ # }
525
+ #
526
+ # # Store the config
527
+ # config_key = subjects.join(',')
528
+ # self.nats_wave_subscription_config[config_key] = subscription_config
529
+ #
530
+ # # Create the subscription handler
531
+ # processed_handler = create_subscription_handler(subscription_config)
532
+ #
533
+ # # Register the subscription
534
+ # NatsWave::ModelRegistry.register_subscription(
535
+ # subjects: subjects.flatten,
536
+ # model: self.name,
537
+ # handler: processed_handler,
538
+ # queue_group: subscription_config[:queue_group]
539
+ # )
540
+ #
541
+ # NatsWave.logger.debug "📡 #{self.name}: Registered subscription for subjects: #{subjects}"
542
+ # end
543
+ #
544
+ # # Configure publishing with optional custom handler AND auto-publishing callbacks
545
+ # def nats_wave_publishes_to(*subjects, **options, &block)
546
+ # NatsWave.logger.debug "📤 #{self.name}: Setting up publishing to subjects: #{subjects.inspect}"
547
+ #
548
+ # # Custom handler is now optional and runs AFTER default behavior
549
+ # custom_handler = options[:handler] || block
550
+ #
551
+ # publishing_config = {
552
+ # subjects: subjects.flatten,
553
+ # field_mappings: options[:field_mappings] || {},
554
+ # transformations: options[:transformations] || {},
555
+ # skip_fields: options[:skip_fields] || [],
556
+ # conditions: options[:conditions] || {},
557
+ # actions: options[:actions] || [:create, :update, :destroy],
558
+ # custom_handler: custom_handler, # Renamed to be clear it's optional
559
+ # async: options[:async] != false, # Default to true
560
+ # enabled: options[:enabled] != false, # Default to true
561
+ # only_mapped_fields: options[:only_mapped_fields] # Changed: don't default to true
562
+ # }
563
+ #
564
+ # # Store the config
565
+ # config_key = subjects.join(',')
566
+ # self.nats_wave_publishing_config[config_key] = publishing_config
567
+ #
568
+ # # Add ActiveRecord callbacks for auto-publishing (only once per model)
569
+ # setup_publishing_callbacks_once
570
+ #
571
+ # NatsWave.logger.debug "📤 #{self.name}: Registered publishing config for subjects: #{subjects}"
572
+ # end
573
+ #
574
+ # # Get all subscription configurations
575
+ # def nats_wave_subscription_configs
576
+ # nats_wave_subscription_config
577
+ # end
578
+ #
579
+ # # Get all publishing configurations
580
+ # def nats_wave_publishing_configs
581
+ # nats_wave_publishing_config
582
+ # end
583
+ #
584
+ # # PUBLIC METHODS - These need to be accessible from lambda context
585
+ #
586
+ # # Process subscription data through field mappings and transformations
587
+ # def process_subscription_data(raw_data, config)
588
+ # processed_data = {}
589
+ # field_mappings = config[:field_mappings]
590
+ # transformations = config[:transformations]
591
+ # skip_fields = config[:skip_fields]
592
+ #
593
+ # raw_data.each do |field, value|
594
+ # field_str = field.to_s
595
+ # field_sym = field.to_sym
596
+ #
597
+ # # Skip if in skip_fields
598
+ # next if skip_fields.include?(field_str) || skip_fields.include?(field_sym)
599
+ #
600
+ # # Apply field mapping (external -> local)
601
+ # mapped_field = field_mappings[field_str] ||
602
+ # field_mappings[field_sym] ||
603
+ # field
604
+ #
605
+ # # Apply transformation if any
606
+ # transformed_value = apply_transformation(value, mapped_field, transformations, raw_data)
607
+ #
608
+ # processed_data[mapped_field.to_s] = transformed_value
609
+ # end
610
+ #
611
+ # processed_data
612
+ # end
613
+ #
614
+ # # Apply transformations to field values
615
+ # def apply_transformation(value, field, transformations, full_record)
616
+ # transformation = transformations[field] || transformations[field.to_sym]
617
+ #
618
+ # case transformation
619
+ # when Proc
620
+ # if transformation.arity == 2 || transformation.arity < 0
621
+ # transformation.call(value, full_record)
622
+ # else
623
+ # transformation.call(value)
624
+ # end
625
+ # when Symbol
626
+ # if self.respond_to?(transformation, true)
627
+ # self.send(transformation, value)
628
+ # elsif value.respond_to?(transformation)
629
+ # value.send(transformation)
630
+ # else
631
+ # NatsWave.logger.warn "Transformation method #{transformation} not found"
632
+ # value
633
+ # end
634
+ # else
635
+ # value
636
+ # end
637
+ # end
638
+ #
639
+ # # Default auto-sync behavior
640
+ # def perform_auto_sync(model_name, action, processed_data, config)
641
+ # unique_fields = config[:unique_fields]
642
+ # sync_strategy = config[:sync_strategy]
643
+ #
644
+ # case action.to_s.downcase
645
+ # when 'create', 'created'
646
+ # handle_auto_create(processed_data, unique_fields, sync_strategy)
647
+ # when 'update', 'updated'
648
+ # handle_auto_update(processed_data, unique_fields, sync_strategy)
649
+ # when 'delete', 'deleted', 'destroy', 'destroyed'
650
+ # handle_auto_delete(processed_data, unique_fields)
651
+ # else
652
+ # NatsWave.logger.warn "Unknown action for auto-sync: #{action}"
653
+ # end
654
+ # end
655
+ #
656
+ # def handle_auto_create(data, unique_fields, sync_strategy)
657
+ # case sync_strategy
658
+ # when :upsert
659
+ # existing = find_by_unique_fields(data, unique_fields)
660
+ # if existing
661
+ # existing.update!(data)
662
+ # NatsWave.logger.info "✅ Updated existing #{self.name}: #{existing.id}"
663
+ # else
664
+ # record = self.create!(data)
665
+ # NatsWave.logger.info "✅ Created new #{self.name}: #{record.id}"
666
+ # end
667
+ # when :create_only
668
+ # record = self.create!(data)
669
+ # NatsWave.logger.info "✅ Created new #{self.name}: #{record.id}"
670
+ # end
671
+ # rescue => e
672
+ # NatsWave.logger.error "❌ Failed to create #{self.name}: #{e.message}"
673
+ # raise
674
+ # end
675
+ #
676
+ # def handle_auto_update(data, unique_fields, sync_strategy)
677
+ # existing = find_by_unique_fields(data, unique_fields)
678
+ # if existing
679
+ # existing.update!(data)
680
+ # NatsWave.logger.info "✅ Updated #{self.name}: #{existing.id}"
681
+ # elsif sync_strategy == :upsert
682
+ # record = self.create!(data)
683
+ # NatsWave.logger.info "✅ Created new #{self.name} during update: #{record.id}"
684
+ # else
685
+ # NatsWave.logger.warn "⚠️ Record not found for update: #{data.slice(*unique_fields.map(&:to_s))}"
686
+ # end
687
+ # rescue => e
688
+ # NatsWave.logger.error "❌ Failed to update #{self.name}: #{e.message}"
689
+ # raise
690
+ # end
691
+ #
692
+ # def handle_auto_delete(data, unique_fields)
693
+ # existing = find_by_unique_fields(data, unique_fields)
694
+ # if existing
695
+ # existing.destroy!
696
+ # NatsWave.logger.info "✅ Deleted #{self.name}: #{existing.id}"
697
+ # else
698
+ # NatsWave.logger.warn "⚠️ Record not found for deletion: #{data.slice(*unique_fields.map(&:to_s))}"
699
+ # end
700
+ # rescue => e
701
+ # NatsWave.logger.error "❌ Failed to delete #{self.name}: #{e.message}"
702
+ # raise
703
+ # end
704
+ #
705
+ # def find_by_unique_fields(data, unique_fields)
706
+ # conditions = {}
707
+ # unique_fields.each do |field|
708
+ # field_str = field.to_s
709
+ # if data.key?(field_str)
710
+ # conditions[field] = data[field_str]
711
+ # elsif data.key?(field.to_sym)
712
+ # conditions[field] = data[field.to_sym]
713
+ # end
714
+ # end
715
+ #
716
+ # return nil if conditions.empty?
717
+ # self.find_by(conditions)
718
+ # end
719
+ #
720
+ # private
721
+ #
722
+ # # Setup ActiveRecord callbacks for auto-publishing (only once per model)
723
+ # def setup_publishing_callbacks_once
724
+ # # Only add callbacks if we're in ActiveRecord and haven't added them yet
725
+ # return unless defined?(ActiveRecord::Base) && self < ActiveRecord::Base
726
+ # return if @nats_wave_callbacks_added
727
+ #
728
+ # after_commit :trigger_nats_wave_auto_publish_on_create, on: :create
729
+ # after_commit :trigger_nats_wave_auto_publish_on_update, on: :update
730
+ # after_commit :trigger_nats_wave_auto_publish_on_destroy, on: :destroy
731
+ #
732
+ # @nats_wave_callbacks_added = true
733
+ # NatsWave.logger.debug "📤 #{self.name}: Added publishing callbacks"
734
+ # end
735
+ #
736
+ # # Create a subscription handler that processes data and calls custom handler AFTER auto-sync
737
+ # def create_subscription_handler(config)
738
+ # model_class = self
739
+ #
740
+ # lambda do |message|
741
+ # begin
742
+ # NatsWave.logger.debug "📨 Processing subscription message for #{model_class.name}"
743
+ #
744
+ # # Extract the raw data
745
+ # raw_data = message['data'] || {}
746
+ # model_name = message['model']
747
+ # action = message['action']
748
+ #
749
+ # # Process the data through mappings and transformations
750
+ # processed_data = model_class.process_subscription_data(raw_data, config)
751
+ #
752
+ # # Always perform auto-sync first (if enabled)
753
+ # if config[:auto_sync]
754
+ # NatsWave.logger.debug "📨 Performing auto-sync first"
755
+ # model_class.perform_auto_sync(model_name, action, processed_data, config)
756
+ # end
757
+ #
758
+ # # Then call custom handler if provided (optional)
759
+ # if config[:custom_handler]
760
+ # NatsWave.logger.debug "📨 Calling custom subscription handler after auto-sync"
761
+ # config[:custom_handler].call(model_name, action, processed_data, message)
762
+ # end
763
+ #
764
+ # rescue => e
765
+ # NatsWave.logger.error "Error in subscription handler for #{model_class.name}: #{e.message}"
766
+ # NatsWave.logger.error e.backtrace.join("\n")
767
+ # raise
768
+ # end
769
+ # end
770
+ # end
771
+ # end
772
+ #
773
+ # # Instance methods for publishing
774
+ # def nats_wave_publish(action = nil)
775
+ # action ||= determine_action_from_context
776
+ #
777
+ # self.class.nats_wave_publishing_configs.each do |subjects_key, config|
778
+ # next unless config[:enabled]
779
+ # next unless config[:actions].include?(action.to_sym)
780
+ #
781
+ # # Check conditions (only for publishing)
782
+ # if config[:conditions].any? && !evaluate_conditions(config[:conditions])
783
+ # NatsWave.logger.debug "📤 Skipping publish - conditions not met"
784
+ # next
785
+ # end
786
+ #
787
+ # # Get the raw data (current model attributes)
788
+ # raw_data = get_raw_attributes
789
+ #
790
+ # # Process the data through mappings and transformations
791
+ # processed_data = process_publishing_data(raw_data, config)
792
+ #
793
+ # # Determine which subjects to publish to based on action
794
+ # subjects_to_publish = determine_subjects_for_action(config[:subjects], action)
795
+ #
796
+ # # Always publish first (default behavior)
797
+ # subjects_to_publish.each do |subject|
798
+ # NatsWave.client.publish(
799
+ # subject: subject,
800
+ # model: self.class.name,
801
+ # action: action,
802
+ # data: processed_data, # This is the processed data, not raw attributes
803
+ # metadata: build_publishing_metadata
804
+ # )
805
+ #
806
+ # NatsWave.logger.info "📤 Published #{self.class.name} to #{subject}"
807
+ # end
808
+ #
809
+ # # Then call custom handler if provided (optional)
810
+ # if config[:custom_handler]
811
+ # NatsWave.logger.debug "📤 Calling custom publishing handler after publishing"
812
+ # config[:custom_handler].call(self.class.name, action, processed_data, self)
813
+ # end
814
+ # end
815
+ # rescue StandardError => e
816
+ # NatsWave.logger.error("Failed to publish: #{e.message}")
817
+ # # Don't re-raise to avoid breaking transactions
818
+ # end
819
+ #
820
+ # # Auto-publishing callback methods (only called once per action)
821
+ # def trigger_nats_wave_auto_publish_on_create
822
+ # return if @nats_wave_publishing_in_progress
823
+ # @nats_wave_publishing_in_progress = true
824
+ #
825
+ # NatsWave.logger.debug "🚀 Auto-publishing on create"
826
+ # nats_wave_publish('create')
827
+ #
828
+ # @nats_wave_publishing_in_progress = false
829
+ # end
830
+ #
831
+ # def trigger_nats_wave_auto_publish_on_update
832
+ # return if @nats_wave_publishing_in_progress
833
+ # @nats_wave_publishing_in_progress = true
834
+ #
835
+ # NatsWave.logger.debug "🚀 Auto-publishing on update"
836
+ # nats_wave_publish('update')
837
+ #
838
+ # @nats_wave_publishing_in_progress = false
839
+ # end
840
+ #
841
+ # def trigger_nats_wave_auto_publish_on_destroy
842
+ # return if @nats_wave_publishing_in_progress
843
+ # @nats_wave_publishing_in_progress = true
844
+ #
845
+ # NatsWave.logger.debug "🚀 Auto-publishing on destroy"
846
+ # nats_wave_publish('destroy')
847
+ #
848
+ # @nats_wave_publishing_in_progress = false
849
+ # end
850
+ #
851
+ # private
852
+ #
853
+ # def determine_action_from_context
854
+ # # Try to determine action from ActiveRecord context
855
+ # if defined?(ActiveRecord) && is_a?(ActiveRecord::Base)
856
+ # return 'create' if previously_new_record?
857
+ # return 'update' if saved_changes.any?
858
+ # return 'destroy' if destroyed?
859
+ # end
860
+ #
861
+ # 'update' # Default fallback
862
+ # end
863
+ #
864
+ # def get_raw_attributes
865
+ # # Get attributes (works for both ActiveRecord and other objects)
866
+ # if respond_to?(:attributes)
867
+ # attributes
868
+ # else
869
+ # # For non-ActiveRecord objects, get instance variables
870
+ # instance_variables.map { |v| [v.to_s.delete('@'), instance_variable_get(v)] }.to_h
871
+ # end
872
+ # end
873
+ #
874
+ # def process_publishing_data(raw_data, config)
875
+ # processed_data = {}
876
+ # field_mappings = config[:field_mappings] || {}
877
+ # transformations = config[:transformations] || {}
878
+ # skip_fields = config[:skip_fields] || []
879
+ # only_mapped_fields = config[:only_mapped_fields]
880
+ #
881
+ # # If only_mapped_fields is true, only process fields that have explicit mappings
882
+ # if only_mapped_fields && field_mappings.any?
883
+ # field_mappings.each do |local_field, external_field|
884
+ # local_field_str = local_field.to_s
885
+ #
886
+ # # Skip if this field is in skip_fields
887
+ # next if skip_fields.include?(local_field_str) || skip_fields.include?(local_field.to_sym)
888
+ #
889
+ # # Get the value from raw_data
890
+ # value = raw_data[local_field_str] || raw_data[local_field.to_sym]
891
+ #
892
+ # # Apply transformation if any (use external field name for transformation lookup)
893
+ # transformed_value = apply_publishing_transformation(value, external_field, transformations, raw_data)
894
+ #
895
+ # processed_data[external_field.to_s] = transformed_value
896
+ # end
897
+ # else
898
+ # # Original behavior - process all fields
899
+ # raw_data.each do |field, value|
900
+ # field_str = field.to_s
901
+ # field_sym = field.to_sym
902
+ #
903
+ # # Skip if in skip_fields
904
+ # next if skip_fields.include?(field_str) || skip_fields.include?(field_sym)
905
+ #
906
+ # # Apply field mapping (local -> external) - ONLY if mapping exists
907
+ # mapped_field = field_mappings[field_str] ||
908
+ # field_mappings[field_sym] ||
909
+ # field # Use original field name if no mapping
910
+ #
911
+ # # Apply transformation if any
912
+ # transformed_value = apply_publishing_transformation(value, mapped_field, transformations, raw_data)
913
+ #
914
+ # processed_data[mapped_field.to_s] = transformed_value
915
+ # end
916
+ # end
917
+ # processed_data
918
+ # end
919
+ #
920
+ # def apply_publishing_transformation(value, field, transformations, full_record)
921
+ # transformation = transformations[field] || transformations[field.to_sym]
922
+ #
923
+ # case transformation
924
+ # when Proc
925
+ # if transformation.arity == 2 || transformation.arity < 0
926
+ # transformation.call(value, full_record)
927
+ # else
928
+ # transformation.call(value)
929
+ # end
930
+ # when Symbol
931
+ # if self.respond_to?(transformation, true)
932
+ # self.send(transformation, value)
933
+ # elsif value.respond_to?(transformation)
934
+ # value.send(transformation)
935
+ # else
936
+ # NatsWave.logger.warn "Publishing transformation method #{transformation} not found"
937
+ # value
938
+ # end
939
+ # else
940
+ # value
941
+ # end
942
+ # end
943
+ #
944
+ # def determine_subjects_for_action(configured_subjects, action)
945
+ # # If subjects contain placeholders like {action}, replace them
946
+ # configured_subjects.map do |subject|
947
+ # if subject.include?('{action}')
948
+ # subject.gsub('{action}', action.to_s)
949
+ # elsif subject.include?('*')
950
+ # # Replace wildcard with specific action
951
+ # subject.gsub('*', action.to_s)
952
+ # else
953
+ # subject
954
+ # end
955
+ # end
956
+ # end
957
+ #
958
+ # def evaluate_conditions(conditions)
959
+ # conditions.all? do |condition, expected_value|
960
+ # case condition
961
+ # when Proc
962
+ # condition.call(self)
963
+ # when Symbol, String
964
+ # if respond_to?(condition, true)
965
+ # result = send(condition)
966
+ # expected_value.nil? ? result : result == expected_value
967
+ # else
968
+ # false
969
+ # end
970
+ # else
971
+ # false
972
+ # end
973
+ # end
974
+ # end
975
+ #
976
+ # def build_publishing_metadata
977
+ # {
978
+ # source_model: self.class.name,
979
+ # source_id: respond_to?(:id) ? id : object_id,
980
+ # published_at: Time.current.iso8601
981
+ # }
982
+ # end
983
+ # end
984
+ # end
985
+ # end
986
+
1
987
  # frozen_string_literal: true
2
988
 
3
989
  module NatsWave
@@ -89,58 +1075,6 @@ module NatsWave
89
1075
  nats_wave_publishing_config
90
1076
  end
91
1077
 
92
- private
93
-
94
- # Setup ActiveRecord callbacks for auto-publishing (only once per model)
95
- def setup_publishing_callbacks_once
96
- # Only add callbacks if we're in ActiveRecord and haven't added them yet
97
- return unless defined?(ActiveRecord::Base) && self < ActiveRecord::Base
98
- return if @nats_wave_callbacks_added
99
-
100
- after_commit :trigger_nats_wave_auto_publish_on_create, on: :create
101
- after_commit :trigger_nats_wave_auto_publish_on_update, on: :update
102
- after_commit :trigger_nats_wave_auto_publish_on_destroy, on: :destroy
103
-
104
- @nats_wave_callbacks_added = true
105
- NatsWave.logger.debug "📤 #{self.name}: Added publishing callbacks"
106
- end
107
-
108
- # Create a subscription handler that processes data and calls custom handler AFTER auto-sync
109
- def create_subscription_handler(config)
110
- model_class = self
111
-
112
- lambda do |message|
113
- begin
114
- NatsWave.logger.debug "📨 Processing subscription message for #{model_class.name}"
115
-
116
- # Extract the raw data
117
- raw_data = message['data'] || {}
118
- model_name = message['model']
119
- action = message['action']
120
-
121
- # Process the data through mappings and transformations
122
- processed_data = model_class.process_subscription_data(raw_data, config)
123
-
124
- # Always perform auto-sync first (if enabled)
125
- if config[:auto_sync]
126
- NatsWave.logger.debug "📨 Performing auto-sync first"
127
- model_class.perform_auto_sync(model_name, action, processed_data, config)
128
- end
129
-
130
- # Then call custom handler if provided (optional)
131
- if config[:custom_handler]
132
- NatsWave.logger.debug "📨 Calling custom subscription handler after auto-sync"
133
- config[:custom_handler].call(model_name, action, processed_data, message)
134
- end
135
-
136
- rescue => e
137
- NatsWave.logger.error "Error in subscription handler for #{model_class.name}: #{e.message}"
138
- NatsWave.logger.error e.backtrace.join("\n")
139
- raise
140
- end
141
- end
142
- end
143
-
144
1078
  # Process subscription data through field mappings and transformations (PUBLIC CLASS METHOD)
145
1079
  def process_subscription_data(raw_data, config)
146
1080
  processed_data = {}
@@ -274,6 +1208,58 @@ module NatsWave
274
1208
  return nil if conditions.empty?
275
1209
  self.find_by(conditions)
276
1210
  end
1211
+
1212
+ private
1213
+
1214
+ # Setup ActiveRecord callbacks for auto-publishing (only once per model)
1215
+ def setup_publishing_callbacks_once
1216
+ # Only add callbacks if we're in ActiveRecord and haven't added them yet
1217
+ return unless defined?(ActiveRecord::Base) && self < ActiveRecord::Base
1218
+ return if @nats_wave_callbacks_added
1219
+
1220
+ after_commit :trigger_nats_wave_auto_publish_on_create, on: :create
1221
+ after_commit :trigger_nats_wave_auto_publish_on_update, on: :update
1222
+ after_commit :trigger_nats_wave_auto_publish_on_destroy, on: :destroy
1223
+
1224
+ @nats_wave_callbacks_added = true
1225
+ NatsWave.logger.debug "📤 #{self.name}: Added publishing callbacks"
1226
+ end
1227
+
1228
+ # Create a subscription handler that processes data and calls custom handler AFTER auto-sync
1229
+ def create_subscription_handler(config)
1230
+ model_class = self
1231
+
1232
+ lambda do |message|
1233
+ begin
1234
+ NatsWave.logger.debug "📨 Processing subscription message for #{model_class.name}"
1235
+
1236
+ # Extract the raw data
1237
+ raw_data = message['data'] || {}
1238
+ model_name = message['model']
1239
+ action = message['action']
1240
+
1241
+ # Process the data through mappings and transformations
1242
+ processed_data = model_class.process_subscription_data(raw_data, config)
1243
+
1244
+ # Always perform auto-sync first (if enabled)
1245
+ if config[:auto_sync]
1246
+ NatsWave.logger.debug "📨 Performing auto-sync first"
1247
+ model_class.perform_auto_sync(model_name, action, processed_data, config)
1248
+ end
1249
+
1250
+ # Then call custom handler if provided (optional)
1251
+ if config[:custom_handler]
1252
+ NatsWave.logger.debug "📨 Calling custom subscription handler after auto-sync"
1253
+ config[:custom_handler].call(model_name, action, processed_data, message)
1254
+ end
1255
+
1256
+ rescue => e
1257
+ NatsWave.logger.error "Error in subscription handler for #{model_class.name}: #{e.message}"
1258
+ NatsWave.logger.error e.backtrace.join("\n")
1259
+ raise
1260
+ end
1261
+ end
1262
+ end
277
1263
  end
278
1264
 
279
1265
  # Instance methods for publishing
@@ -18,7 +18,7 @@ module NatsWave
18
18
  def initialize(options = {})
19
19
  @nats_url = ENV['NATS_URL'] || "nats://localhost:4222"
20
20
  @service_name = ENV['NATS_SERVICE_NAME'] || "purplewave"
21
- @version = ENV['NATS_SERVICE_VERSION'] || "1.1.14"
21
+ @version = ENV['NATS_SERVICE_VERSION'] || "1.1.15"
22
22
  @instance_id = ENV['NATS_INSTANCE_ID'] || Socket.gethostname
23
23
  @database_url = ENV['NATS_DATABASE_URL'] || nil
24
24
  @connection_pool_size = (ENV['NATS_CONNECTION_POOL_SIZE'] || 10).to_i
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module NatsWave
4
- VERSION = '1.1.14'
4
+ VERSION = '1.1.15'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nats_wave
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.14
4
+ version: 1.1.15
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jeffrey Dabo
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-07-26 00:00:00.000000000 Z
11
+ date: 2025-07-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: nats-pure