nats_wave 1.1.10 → 1.1.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/lib/nats_wave/adapters/datadog_metrics.rb +1 -1
- data/lib/nats_wave/concerns/mappable.rb +918 -154
- data/lib/nats_wave/configuration.rb +1 -1
- data/lib/nats_wave/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b0f72efa07e504282e4a42fd5a21d526272266d2fa119b1032d867f27847e1eb
|
4
|
+
data.tar.gz: 656647fe9ab1c21470c06240f3a6448e97749855ed939557ccd9df58fb684a1c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 74f5fe30f7d8271bb8de826057a9b8cdc67e4af796f60e56178259fa7bfa71feae3f116d2d1ed0e7ff5bc004256340d8845bbe4229f9cadf85827a7112678a16
|
7
|
+
data.tar.gz: 429da1483506f2665b940f0864bcf22f0699c90879ad937c844c9fc071eeb9557576b2dfbc3fa9dc3124b04e98d71c152f40282f96269d2f25a089d0ce123be3
|
data/Gemfile.lock
CHANGED
@@ -1,3 +1,367 @@
|
|
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
|
+
#
|
1
365
|
# # frozen_string_literal: true
|
2
366
|
#
|
3
367
|
# module NatsWave
|
@@ -6,173 +370,449 @@
|
|
6
370
|
# extend ActiveSupport::Concern
|
7
371
|
#
|
8
372
|
# included do
|
9
|
-
# class_attribute :
|
10
|
-
# self.nats_wave_mapping_config = {}
|
373
|
+
# class_attribute :nats_wave_subscription_config, :nats_wave_publishing_config
|
11
374
|
# self.nats_wave_subscription_config = {}
|
375
|
+
# self.nats_wave_publishing_config = {}
|
12
376
|
# end
|
13
377
|
#
|
14
378
|
# class_methods do
|
15
|
-
# # Configure
|
16
|
-
# def
|
17
|
-
# self.
|
18
|
-
#
|
19
|
-
# # Register this mapping globally
|
20
|
-
# NatsWave::ModelRegistry.register_mapping(self.name, external_models)
|
21
|
-
# end
|
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)
|
22
382
|
#
|
23
|
-
#
|
24
|
-
# def nats_wave_maps_from(external_model, options = {})
|
25
|
-
# Rails.logger.debug "🔄 #{self.name}: Setting up mapping from #{external_model} with options: #{options.inspect}" if defined?(Rails)
|
383
|
+
# handler = options[:handler] || block
|
26
384
|
#
|
27
|
-
#
|
385
|
+
# subscription_config = {
|
386
|
+
# subjects: subjects.flatten,
|
28
387
|
# field_mappings: options[:field_mappings] || {},
|
29
388
|
# transformations: options[:transformations] || {},
|
30
|
-
# conditions: options[:conditions] || {},
|
31
|
-
# sync_strategy: options[:sync_strategy] || :upsert,
|
32
|
-
# unique_fields: options[:unique_fields] || [:id],
|
33
389
|
# skip_fields: options[:skip_fields] || [],
|
34
|
-
#
|
35
|
-
#
|
36
|
-
#
|
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
|
37
395
|
# }
|
38
396
|
#
|
39
|
-
#
|
40
|
-
#
|
41
|
-
#
|
42
|
-
#
|
43
|
-
#
|
44
|
-
#
|
45
|
-
#
|
46
|
-
#
|
47
|
-
#
|
48
|
-
#
|
49
|
-
#
|
50
|
-
#
|
51
|
-
#
|
52
|
-
#
|
53
|
-
#
|
54
|
-
#
|
55
|
-
# Rails.logger.debug "🔄 #{self.name}: No subjects provided, skipping subscription registration" if defined?(Rails)
|
56
|
-
# end
|
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)
|
57
413
|
# end
|
58
414
|
#
|
59
|
-
# # Configure
|
60
|
-
# def
|
61
|
-
#
|
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)
|
62
418
|
#
|
63
|
-
#
|
419
|
+
# handler = options[:handler] || block
|
64
420
|
#
|
65
|
-
#
|
421
|
+
# publishing_config = {
|
66
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],
|
67
428
|
# handler: handler,
|
68
|
-
#
|
69
|
-
#
|
429
|
+
# async: options[:async] != false, # Default to true
|
430
|
+
# enabled: options[:enabled] != false # Default to true
|
70
431
|
# }
|
71
432
|
#
|
72
|
-
#
|
433
|
+
# # Store the config
|
434
|
+
# config_key = subjects.join(',')
|
435
|
+
# self.nats_wave_publishing_config[config_key] = publishing_config
|
73
436
|
#
|
74
|
-
#
|
75
|
-
#
|
76
|
-
#
|
77
|
-
#
|
78
|
-
# handler: handler,
|
79
|
-
# queue_group: queue_group
|
80
|
-
# )
|
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)
|
81
441
|
# end
|
82
442
|
#
|
83
|
-
# #
|
84
|
-
# def
|
85
|
-
#
|
443
|
+
# # Get all subscription configurations
|
444
|
+
# def nats_wave_subscription_configs
|
445
|
+
# nats_wave_subscription_config
|
446
|
+
# end
|
86
447
|
#
|
87
|
-
#
|
88
|
-
#
|
89
|
-
#
|
90
|
-
#
|
91
|
-
#
|
92
|
-
#
|
93
|
-
#
|
94
|
-
#
|
95
|
-
#
|
96
|
-
#
|
97
|
-
#
|
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
|
98
544
|
# end
|
99
545
|
#
|
100
|
-
#
|
546
|
+
# processed_data
|
101
547
|
# end
|
102
548
|
#
|
103
|
-
# #
|
104
|
-
# def
|
105
|
-
#
|
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
|
106
572
|
# end
|
107
573
|
#
|
108
|
-
# #
|
109
|
-
# def
|
110
|
-
#
|
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
|
111
589
|
# end
|
112
590
|
#
|
113
|
-
#
|
114
|
-
#
|
115
|
-
#
|
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
|
116
609
|
# end
|
117
610
|
#
|
118
|
-
#
|
119
|
-
#
|
120
|
-
#
|
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
|
121
626
|
#
|
122
|
-
#
|
123
|
-
#
|
124
|
-
#
|
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)
|
125
634
|
# end
|
635
|
+
# rescue => e
|
636
|
+
# Rails.logger.error "❌ Failed to delete #{self.name}: #{e.message}" if defined?(Rails)
|
637
|
+
# raise
|
638
|
+
# end
|
126
639
|
#
|
127
|
-
#
|
128
|
-
#
|
129
|
-
#
|
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
|
130
649
|
# end
|
131
650
|
#
|
132
|
-
#
|
651
|
+
# return nil if conditions.empty?
|
652
|
+
# self.find_by(conditions)
|
133
653
|
# end
|
134
654
|
# end
|
135
655
|
#
|
136
|
-
# # Instance methods
|
137
|
-
# def
|
138
|
-
#
|
139
|
-
#
|
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
|
140
672
|
#
|
141
|
-
#
|
142
|
-
#
|
143
|
-
# skip_fields = mapping[:skip_fields] || []
|
673
|
+
# # Process the data through mappings and transformations
|
674
|
+
# processed_data = process_publishing_data(raw_data, config)
|
144
675
|
#
|
145
|
-
#
|
146
|
-
#
|
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
|
+
# )
|
147
690
|
#
|
148
|
-
#
|
149
|
-
#
|
691
|
+
# Rails.logger.info "📤 Published #{self.class.name}##{id} to #{subject} (#{action})" if defined?(Rails)
|
692
|
+
# end
|
693
|
+
# end
|
150
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
|
151
699
|
#
|
152
|
-
#
|
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')
|
153
704
|
# end
|
154
705
|
#
|
155
|
-
# def
|
156
|
-
#
|
157
|
-
#
|
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
|
158
710
|
#
|
159
|
-
#
|
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')
|
160
714
|
# end
|
161
715
|
#
|
162
716
|
# private
|
163
717
|
#
|
164
|
-
# def
|
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)
|
165
768
|
# transformation = transformations[field] || transformations[field.to_sym]
|
166
769
|
#
|
167
770
|
# case transformation
|
168
771
|
# when Proc
|
169
|
-
# transformation.
|
772
|
+
# if transformation.arity == 2 || transformation.arity < 0
|
773
|
+
# transformation.call(value, full_record)
|
774
|
+
# else
|
775
|
+
# transformation.call(value)
|
776
|
+
# end
|
170
777
|
# when Symbol
|
171
|
-
#
|
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
|
172
786
|
# else
|
173
787
|
# value
|
174
788
|
# end
|
175
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
|
176
816
|
# end
|
177
817
|
# end
|
178
818
|
# end
|
@@ -195,7 +835,8 @@ module NatsWave
|
|
195
835
|
def nats_wave_subscribes_to(*subjects, **options, &block)
|
196
836
|
Rails.logger.debug "📡 #{self.name}: Setting up subscription to subjects: #{subjects.inspect}" if defined?(Rails)
|
197
837
|
|
198
|
-
|
838
|
+
# Custom handler is now optional and runs AFTER default behavior
|
839
|
+
custom_handler = options[:handler] || block
|
199
840
|
|
200
841
|
subscription_config = {
|
201
842
|
subjects: subjects.flatten,
|
@@ -204,7 +845,7 @@ module NatsWave
|
|
204
845
|
skip_fields: options[:skip_fields] || [],
|
205
846
|
unique_fields: options[:unique_fields] || [:id],
|
206
847
|
sync_strategy: options[:sync_strategy] || :upsert,
|
207
|
-
|
848
|
+
custom_handler: custom_handler, # Renamed to be clear it's optional
|
208
849
|
queue_group: options[:queue_group],
|
209
850
|
auto_sync: options[:auto_sync] != false # Default to true, can be disabled
|
210
851
|
}
|
@@ -227,11 +868,12 @@ module NatsWave
|
|
227
868
|
Rails.logger.debug "📡 #{self.name}: Registered subscription for subjects: #{subjects}" if defined?(Rails)
|
228
869
|
end
|
229
870
|
|
230
|
-
# Configure publishing with optional custom handler
|
871
|
+
# Configure publishing with optional custom handler AND auto-publishing callbacks
|
231
872
|
def nats_wave_publishes_to(*subjects, **options, &block)
|
232
873
|
Rails.logger.debug "📤 #{self.name}: Setting up publishing to subjects: #{subjects.inspect}" if defined?(Rails)
|
233
874
|
|
234
|
-
|
875
|
+
# Custom handler is now optional and runs AFTER default behavior
|
876
|
+
custom_handler = options[:handler] || block
|
235
877
|
|
236
878
|
publishing_config = {
|
237
879
|
subjects: subjects.flatten,
|
@@ -240,15 +882,19 @@ module NatsWave
|
|
240
882
|
skip_fields: options[:skip_fields] || [],
|
241
883
|
conditions: options[:conditions] || {},
|
242
884
|
actions: options[:actions] || [:create, :update, :destroy],
|
243
|
-
|
885
|
+
custom_handler: custom_handler, # Renamed to be clear it's optional
|
244
886
|
async: options[:async] != false, # Default to true
|
245
|
-
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
|
246
889
|
}
|
247
890
|
|
248
891
|
# Store the config
|
249
892
|
config_key = subjects.join(',')
|
250
893
|
self.nats_wave_publishing_config[config_key] = publishing_config
|
251
894
|
|
895
|
+
# Add ActiveRecord callbacks for auto-publishing (only once per model)
|
896
|
+
setup_publishing_callbacks_once
|
897
|
+
|
252
898
|
Rails.logger.debug "📤 #{self.name}: Registered publishing config for subjects: #{subjects}" if defined?(Rails)
|
253
899
|
end
|
254
900
|
|
@@ -262,19 +908,23 @@ module NatsWave
|
|
262
908
|
nats_wave_publishing_config
|
263
909
|
end
|
264
910
|
|
265
|
-
|
266
|
-
def nats_wave_subscribed_subjects
|
267
|
-
nats_wave_subscription_config.values.flat_map { |config| config[:subjects] }.uniq
|
268
|
-
end
|
911
|
+
private
|
269
912
|
|
270
|
-
#
|
271
|
-
def
|
272
|
-
|
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
|
918
|
+
|
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
|
922
|
+
|
923
|
+
@nats_wave_callbacks_added = true
|
924
|
+
Rails.logger.debug "📤 #{self.name}: Added publishing callbacks" if defined?(Rails)
|
273
925
|
end
|
274
926
|
|
275
|
-
|
276
|
-
|
277
|
-
# Create a subscription handler that processes data and calls custom handler or auto-sync
|
927
|
+
# Create a subscription handler that processes data and calls custom handler AFTER auto-sync
|
278
928
|
def create_subscription_handler(config)
|
279
929
|
model_class = self
|
280
930
|
|
@@ -288,18 +938,18 @@ module NatsWave
|
|
288
938
|
action = message['action']
|
289
939
|
|
290
940
|
# Process the data through mappings and transformations
|
291
|
-
processed_data =
|
941
|
+
processed_data = process_subscription_data(raw_data, config)
|
292
942
|
|
293
|
-
#
|
294
|
-
if config[:
|
295
|
-
Rails.logger.debug "📨
|
296
|
-
config[:handler].call(model_name, action, processed_data, message)
|
297
|
-
elsif config[:auto_sync]
|
298
|
-
Rails.logger.debug "📨 Performing auto-sync" if defined?(Rails)
|
299
|
-
# Default auto-sync behavior
|
943
|
+
# Always perform auto-sync first (if enabled)
|
944
|
+
if config[:auto_sync]
|
945
|
+
Rails.logger.debug "📨 Performing auto-sync first" if defined?(Rails)
|
300
946
|
model_class.perform_auto_sync(model_name, action, processed_data, config)
|
301
|
-
|
302
|
-
|
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)
|
303
953
|
end
|
304
954
|
|
305
955
|
rescue => e
|
@@ -310,8 +960,8 @@ module NatsWave
|
|
310
960
|
end
|
311
961
|
end
|
312
962
|
|
313
|
-
# Process data through field mappings and transformations (
|
314
|
-
def
|
963
|
+
# Process subscription data through field mappings and transformations (CLASS METHOD)
|
964
|
+
def process_subscription_data(raw_data, config)
|
315
965
|
processed_data = {}
|
316
966
|
field_mappings = config[:field_mappings]
|
317
967
|
transformations = config[:transformations]
|
@@ -338,7 +988,7 @@ module NatsWave
|
|
338
988
|
processed_data
|
339
989
|
end
|
340
990
|
|
341
|
-
# Apply transformations to field values
|
991
|
+
# Apply transformations to field values (CLASS METHOD)
|
342
992
|
def apply_transformation(value, field, transformations, full_record)
|
343
993
|
transformation = transformations[field] || transformations[field.to_sym]
|
344
994
|
|
@@ -363,7 +1013,7 @@ module NatsWave
|
|
363
1013
|
end
|
364
1014
|
end
|
365
1015
|
|
366
|
-
# Default auto-sync behavior
|
1016
|
+
# Default auto-sync behavior (CLASS METHOD)
|
367
1017
|
def perform_auto_sync(model_name, action, processed_data, config)
|
368
1018
|
unique_fields = config[:unique_fields]
|
369
1019
|
sync_strategy = config[:sync_strategy]
|
@@ -462,26 +1112,29 @@ module NatsWave
|
|
462
1112
|
# Get the raw data (current model attributes)
|
463
1113
|
raw_data = get_raw_attributes
|
464
1114
|
|
465
|
-
# Process the data through mappings and transformations
|
466
|
-
processed_data =
|
1115
|
+
# Process the data through mappings and transformations
|
1116
|
+
processed_data = process_publishing_data(raw_data, config)
|
467
1117
|
|
468
|
-
#
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
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)
|
485
1138
|
end
|
486
1139
|
end
|
487
1140
|
rescue StandardError => e
|
@@ -489,6 +1142,37 @@ module NatsWave
|
|
489
1142
|
# Don't re-raise to avoid breaking transactions
|
490
1143
|
end
|
491
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
|
+
|
492
1176
|
private
|
493
1177
|
|
494
1178
|
def determine_action_from_context
|
@@ -512,6 +1196,86 @@ module NatsWave
|
|
512
1196
|
end
|
513
1197
|
end
|
514
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
|
+
|
515
1279
|
def evaluate_conditions(conditions)
|
516
1280
|
conditions.all? do |condition, expected_value|
|
517
1281
|
case condition
|
@@ -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.
|
21
|
+
@version = ENV['NATS_SERVICE_VERSION'] || "1.1.12"
|
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
|
data/lib/nats_wave/version.rb
CHANGED