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