nats_wave 1.1.14 → 1.1.16
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/.idea/nats_wave.iml +5 -5
- data/Gemfile.lock +1 -1
- data/lib/nats_wave/adapters/datadog_metrics.rb +1 -1
- data/lib/nats_wave/concerns/mappable.rb +1057 -56
- data/lib/nats_wave/configuration.rb +1 -1
- data/lib/nats_wave/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 70b9abe2ec70949d2ffd0b97d0d2e33688590d8d4bc5c01baa2a555cb0b355e4
|
4
|
+
data.tar.gz: cbef4f7b640670744b71626606a0ad62de313012bfb524e7e406594ff0b31e9c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 552e1c4272994c73ab29ed2c714aa7d9e776ef6cd81431a97da99af9609669815ad8af64651a12278154609033f1a1e82c37a9fa921ce400770a2228f56078bb
|
7
|
+
data.tar.gz: 6c2f1a722122a2de176186b58835158764ff0ba4a5913c7d15e0b15b8b15feaba6b053fd12a97acc8d0699b3b2db7f701797679efe5322574e43c1b86b4cd6fd
|
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.
|
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.
|
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.
|
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.
|
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.
|
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,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
|
@@ -51,7 +1037,7 @@ module NatsWave
|
|
51
1037
|
|
52
1038
|
# Configure publishing with optional custom handler AND auto-publishing callbacks
|
53
1039
|
def nats_wave_publishes_to(*subjects, **options, &block)
|
54
|
-
NatsWave.logger.debug "
|
1040
|
+
NatsWave.logger.debug "⚙️#{self.name}: Setting up publishing to subjects: #{subjects.inspect}"
|
55
1041
|
|
56
1042
|
# Custom handler is now optional and runs AFTER default behavior
|
57
1043
|
custom_handler = options[:handler] || block
|
@@ -76,7 +1062,7 @@ module NatsWave
|
|
76
1062
|
# Add ActiveRecord callbacks for auto-publishing (only once per model)
|
77
1063
|
setup_publishing_callbacks_once
|
78
1064
|
|
79
|
-
NatsWave.logger.debug "
|
1065
|
+
NatsWave.logger.debug "⚙️#{self.name}: Registered publishing config for subjects: #{subjects}"
|
80
1066
|
end
|
81
1067
|
|
82
1068
|
# Get all subscription configurations
|
@@ -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,73 @@ 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
|
+
# If action is empty, try to extract from subject
|
1242
|
+
if action.blank? && message['subject']
|
1243
|
+
# Extract action from subject like "ims.item.create" -> "create"
|
1244
|
+
subject_parts = message['subject'].split('.')
|
1245
|
+
action = subject_parts.last if subject_parts.any?
|
1246
|
+
end
|
1247
|
+
|
1248
|
+
NatsWave.logger.debug "✅Extracted action: '#{action}' from message"
|
1249
|
+
|
1250
|
+
# Handle nested data structure
|
1251
|
+
if raw_data['data'].is_a?(Hash)
|
1252
|
+
raw_data = raw_data['data']
|
1253
|
+
NatsWave.logger.debug "⚙️Using nested data structure"
|
1254
|
+
end
|
1255
|
+
|
1256
|
+
# Process the data through mappings and transformations
|
1257
|
+
processed_data = model_class.process_subscription_data(raw_data, config)
|
1258
|
+
|
1259
|
+
# Always perform auto-sync first (if enabled)
|
1260
|
+
if config[:auto_sync]
|
1261
|
+
NatsWave.logger.debug "⚙️Performing auto-sync first"
|
1262
|
+
model_class.perform_auto_sync(model_name, action, processed_data, config)
|
1263
|
+
end
|
1264
|
+
|
1265
|
+
# Then call custom handler if provided (optional)
|
1266
|
+
if config[:custom_handler]
|
1267
|
+
NatsWave.logger.debug "⚙️Calling custom subscription handler after auto-sync"
|
1268
|
+
config[:custom_handler].call(model_name, action, processed_data, message)
|
1269
|
+
end
|
1270
|
+
|
1271
|
+
rescue => e
|
1272
|
+
NatsWave.logger.error "Error in subscription handler for #{model_class.name}: #{e.message}"
|
1273
|
+
NatsWave.logger.error e.backtrace.join("\n")
|
1274
|
+
raise
|
1275
|
+
end
|
1276
|
+
end
|
1277
|
+
end
|
277
1278
|
end
|
278
1279
|
|
279
1280
|
# Instance methods for publishing
|
@@ -286,7 +1287,7 @@ module NatsWave
|
|
286
1287
|
|
287
1288
|
# Check conditions (only for publishing)
|
288
1289
|
if config[:conditions].any? && !evaluate_conditions(config[:conditions])
|
289
|
-
NatsWave.logger.debug "
|
1290
|
+
NatsWave.logger.debug "⚙️Skipping publish - conditions not met"
|
290
1291
|
next
|
291
1292
|
end
|
292
1293
|
|
@@ -314,7 +1315,7 @@ module NatsWave
|
|
314
1315
|
|
315
1316
|
# Then call custom handler if provided (optional)
|
316
1317
|
if config[:custom_handler]
|
317
|
-
NatsWave.logger.debug "
|
1318
|
+
NatsWave.logger.debug "⚙️Calling custom publishing handler after publishing"
|
318
1319
|
config[:custom_handler].call(self.class.name, action, processed_data, self)
|
319
1320
|
end
|
320
1321
|
end
|
@@ -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.16"
|
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
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.
|
4
|
+
version: 1.1.16
|
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-
|
11
|
+
date: 2025-07-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: nats-pure
|