nats_wave 1.1.0
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 +7 -0
- data/.idea/.gitignore +8 -0
- data/.idea/misc.xml +4 -0
- data/.idea/modules.xml +8 -0
- data/.idea/nats_wave.iml +169 -0
- data/.idea/vcs.xml +6 -0
- data/.rspec +3 -0
- data/.rubocop.yml +16 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +136 -0
- data/Gemfile +15 -0
- data/Gemfile.lock +332 -0
- data/LICENSE.txt +21 -0
- data/README.md +985 -0
- data/Rakefile +12 -0
- data/config/nats_wave.yml +65 -0
- data/examples/catalog_model.rb +36 -0
- data/examples/configuration_examples.rb +68 -0
- data/examples/user_model.rb +58 -0
- data/lib/generators/nats_wave/install_generator.rb +40 -0
- data/lib/generators/nats_wave/templates/README +31 -0
- data/lib/generators/nats_wave/templates/create_nats_wave_failed_messages.rb +20 -0
- data/lib/generators/nats_wave/templates/create_nats_wave_failed_subscriptions.rb +20 -0
- data/lib/generators/nats_wave/templates/initializer.rb +64 -0
- data/lib/generators/nats_wave/templates/nats_wave.yml +65 -0
- data/lib/nats_wave/adapters/active_record.rb +206 -0
- data/lib/nats_wave/adapters/datadog_metrics.rb +93 -0
- data/lib/nats_wave/auto_registration.rb +109 -0
- data/lib/nats_wave/client.rb +142 -0
- data/lib/nats_wave/concerns/mappable.rb +172 -0
- data/lib/nats_wave/concerns/publishable.rb +216 -0
- data/lib/nats_wave/configuration.rb +105 -0
- data/lib/nats_wave/database_connector.rb +50 -0
- data/lib/nats_wave/dead_letter_queue.rb +146 -0
- data/lib/nats_wave/errors.rb +27 -0
- data/lib/nats_wave/message_transformer.rb +95 -0
- data/lib/nats_wave/metrics.rb +220 -0
- data/lib/nats_wave/middleware/authentication.rb +65 -0
- data/lib/nats_wave/middleware/base.rb +19 -0
- data/lib/nats_wave/middleware/logging.rb +58 -0
- data/lib/nats_wave/middleware/validation.rb +74 -0
- data/lib/nats_wave/model_mapper.rb +125 -0
- data/lib/nats_wave/model_registry.rb +150 -0
- data/lib/nats_wave/publisher.rb +151 -0
- data/lib/nats_wave/railtie.rb +111 -0
- data/lib/nats_wave/schema_registry.rb +77 -0
- data/lib/nats_wave/subscriber.rb +161 -0
- data/lib/nats_wave/version.rb +5 -0
- data/lib/nats_wave.rb +97 -0
- data/lib/tasks/nats_wave.rake +360 -0
- data/sig/nats_wave.rbs +5 -0
- metadata +385 -0
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'datadog/statsd'
|
4
|
+
|
5
|
+
module NatsWave
|
6
|
+
module Adapters
|
7
|
+
class DatadogMetrics
|
8
|
+
def initialize(config = {})
|
9
|
+
@statsd = Datadog::Statsd.new(
|
10
|
+
config[:host] || ENV['DD_AGENT_HOST'] || 'localhost',
|
11
|
+
config[:port] || ENV['DD_AGENT_PORT'] || 8125,
|
12
|
+
tags: default_tags.merge(config[:tags] || {}),
|
13
|
+
namespace: config[:namespace] || 'nats_wave'
|
14
|
+
)
|
15
|
+
@service_name = config[:service_name] || 'unknown'
|
16
|
+
end
|
17
|
+
|
18
|
+
def increment(metric_name, tags: [], value: 1)
|
19
|
+
formatted_tags = format_tags(tags)
|
20
|
+
|
21
|
+
case metric_name
|
22
|
+
when 'nats_wave.messages.published'
|
23
|
+
@statsd.increment('messages.published', value, tags: formatted_tags)
|
24
|
+
when 'nats_wave.messages.received'
|
25
|
+
@statsd.increment('messages.received', value, tags: formatted_tags)
|
26
|
+
when 'nats_wave.messages.failed'
|
27
|
+
@statsd.increment('messages.failed', value, tags: formatted_tags)
|
28
|
+
when 'nats_wave.errors'
|
29
|
+
@statsd.increment('errors', value, tags: formatted_tags)
|
30
|
+
when 'nats_wave.retries'
|
31
|
+
@statsd.increment('retries', value, tags: formatted_tags)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def histogram(metric_name, value, tags: [])
|
36
|
+
formatted_tags = format_tags(tags)
|
37
|
+
|
38
|
+
case metric_name
|
39
|
+
when 'nats_wave.processing_time'
|
40
|
+
@statsd.histogram('processing_time', value, tags: formatted_tags)
|
41
|
+
when 'nats_wave.message_size'
|
42
|
+
@statsd.histogram('message_size', value, tags: formatted_tags)
|
43
|
+
when 'nats_wave.queue_depth'
|
44
|
+
@statsd.histogram('queue_depth', value, tags: formatted_tags)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def gauge(metric_name, value, tags: [])
|
49
|
+
formatted_tags = format_tags(tags)
|
50
|
+
|
51
|
+
case metric_name
|
52
|
+
when 'nats_wave.connection_status'
|
53
|
+
@statsd.gauge('connection_status', value, tags: formatted_tags)
|
54
|
+
when 'nats_wave.subscriber_count'
|
55
|
+
@statsd.gauge('subscriber_count', value, tags: formatted_tags)
|
56
|
+
when 'nats_wave.failed_messages_count'
|
57
|
+
@statsd.gauge('failed_messages_count', value, tags: formatted_tags)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def timing(metric_name, &block)
|
62
|
+
formatted_tags = format_tags([])
|
63
|
+
|
64
|
+
case metric_name
|
65
|
+
when 'nats_wave.operation_time'
|
66
|
+
@statsd.time('operation_time', tags: formatted_tags, &block)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def close
|
71
|
+
@statsd.close if @statsd.respond_to?(:close)
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
def default_tags
|
77
|
+
{
|
78
|
+
service: @service_name,
|
79
|
+
environment: ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'unknown',
|
80
|
+
version: ENV['APP_VERSION'] || '1.1.0'
|
81
|
+
}
|
82
|
+
end
|
83
|
+
|
84
|
+
def format_tags(tags)
|
85
|
+
# Convert ["subject:user.create", "status:success"] to ["subject:user.create", "status:success"]
|
86
|
+
# Datadog expects this format
|
87
|
+
Array(tags).map do |tag|
|
88
|
+
tag.is_a?(String) ? tag : "#{tag[:key]}:#{tag[:value]}"
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module NatsWave
|
4
|
+
class AutoRegistration
|
5
|
+
class << self
|
6
|
+
def register_all_models!
|
7
|
+
return unless defined?(Rails)
|
8
|
+
|
9
|
+
Rails.logger.info "🔍 Auto-registering NatsWave models..."
|
10
|
+
|
11
|
+
# Load all models
|
12
|
+
load_all_models
|
13
|
+
|
14
|
+
# Find models with NatsWave mappings
|
15
|
+
mappable_models = find_mappable_models
|
16
|
+
|
17
|
+
Rails.logger.info "📋 Found #{mappable_models.size} models with NatsWave mappings"
|
18
|
+
|
19
|
+
# Register their subscriptions with the client
|
20
|
+
register_model_subscriptions(mappable_models)
|
21
|
+
|
22
|
+
# Log registration summary
|
23
|
+
log_registration_summary
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def load_all_models
|
29
|
+
# Ensure all models are loaded
|
30
|
+
Rails.application.eager_load! if Rails.env.development?
|
31
|
+
|
32
|
+
# Also try to load models manually
|
33
|
+
Dir[Rails.root.join('app/models/**/*.rb')].each do |file|
|
34
|
+
require_dependency file
|
35
|
+
rescue => e
|
36
|
+
Rails.logger.warn "Could not load model file #{file}: #{e.message}"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def find_mappable_models
|
41
|
+
models = []
|
42
|
+
|
43
|
+
ActiveRecord::Base.descendants.each do |model_class|
|
44
|
+
next unless model_class.respond_to?(:nats_wave_subscribed_subjects)
|
45
|
+
next if model_class.nats_wave_subscribed_subjects.empty?
|
46
|
+
|
47
|
+
models << model_class
|
48
|
+
end
|
49
|
+
|
50
|
+
models
|
51
|
+
end
|
52
|
+
|
53
|
+
def register_model_subscriptions(models)
|
54
|
+
models.each do |model_class|
|
55
|
+
begin
|
56
|
+
register_model_subscription(model_class)
|
57
|
+
rescue => e
|
58
|
+
Rails.logger.error "Failed to register subscriptions for #{model_class.name}: #{e.message}"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def register_model_subscription(model_class)
|
64
|
+
subjects = model_class.nats_wave_subscribed_subjects
|
65
|
+
return if subjects.empty?
|
66
|
+
|
67
|
+
Rails.logger.debug "📡 Registering #{model_class.name} subscriptions: #{subjects.join(', ')}"
|
68
|
+
|
69
|
+
# Register with NATS client
|
70
|
+
NatsWave.client.subscribe(
|
71
|
+
subjects: subjects,
|
72
|
+
model_mappings: build_model_mappings_for(model_class)
|
73
|
+
)
|
74
|
+
end
|
75
|
+
|
76
|
+
def build_model_mappings_for(model_class)
|
77
|
+
mappings = {}
|
78
|
+
|
79
|
+
model_class.nats_wave_external_models.each do |external_model|
|
80
|
+
mapping_config = model_class.nats_wave_mapping_for(external_model)
|
81
|
+
|
82
|
+
mappings[external_model] = {
|
83
|
+
target_model: model_class.name,
|
84
|
+
field_mappings: mapping_config[:field_mappings] || {},
|
85
|
+
transformations: mapping_config[:transformations] || {},
|
86
|
+
conditions: mapping_config[:conditions] || {},
|
87
|
+
sync_strategy: mapping_config[:sync_strategy] || :upsert,
|
88
|
+
unique_fields: mapping_config[:unique_fields] || [:id]
|
89
|
+
}
|
90
|
+
end
|
91
|
+
|
92
|
+
mappings
|
93
|
+
end
|
94
|
+
|
95
|
+
def log_registration_summary
|
96
|
+
stats = ModelRegistry.subscription_stats
|
97
|
+
|
98
|
+
Rails.logger.info "📊 NatsWave Registration Summary:"
|
99
|
+
Rails.logger.info " Total Subscriptions: #{stats[:total_subscriptions]}"
|
100
|
+
Rails.logger.info " Unique Subjects: #{stats[:unique_subjects]}"
|
101
|
+
Rails.logger.info " Models with Subscriptions: #{stats[:models_with_subscriptions]}"
|
102
|
+
|
103
|
+
stats[:subscription_breakdown].each do |model, count|
|
104
|
+
Rails.logger.info " #{model}: #{count} subscription(s)"
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,142 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module NatsWave
|
4
|
+
class Client
|
5
|
+
attr_reader :config, :publisher, :subscriber, :nats_client
|
6
|
+
|
7
|
+
def initialize(options = {})
|
8
|
+
@config = Configuration.new(options)
|
9
|
+
@nats_client = nil
|
10
|
+
@publisher = nil
|
11
|
+
@subscriber = nil
|
12
|
+
@middleware_stack = []
|
13
|
+
|
14
|
+
setup_connection_pool
|
15
|
+
setup_middleware
|
16
|
+
establish_connections
|
17
|
+
end
|
18
|
+
|
19
|
+
def publish(subject:, model:, action:, data:, metadata: {})
|
20
|
+
ensure_connected!
|
21
|
+
@publisher.publish(
|
22
|
+
subject: subject,
|
23
|
+
model: model,
|
24
|
+
action: action,
|
25
|
+
data: data,
|
26
|
+
metadata: metadata
|
27
|
+
)
|
28
|
+
end
|
29
|
+
|
30
|
+
def publish_batch(events)
|
31
|
+
ensure_connected!
|
32
|
+
@publisher.publish_batch(events)
|
33
|
+
end
|
34
|
+
|
35
|
+
def subscribe(subjects:, model_mappings: {}, &block)
|
36
|
+
ensure_connected!
|
37
|
+
@subscriber.subscribe(
|
38
|
+
subjects: subjects,
|
39
|
+
model_mappings: model_mappings,
|
40
|
+
handler: block
|
41
|
+
)
|
42
|
+
end
|
43
|
+
|
44
|
+
def start_subscriber
|
45
|
+
ensure_connected!
|
46
|
+
@subscriber.start if @subscriber
|
47
|
+
end
|
48
|
+
|
49
|
+
def health_check
|
50
|
+
{
|
51
|
+
nats_connected: connected?,
|
52
|
+
database_connected: database_connected?,
|
53
|
+
nats_url: @config.nats_url,
|
54
|
+
nats_server_url: @config.nats_server_url,
|
55
|
+
service_name: @config.service_name,
|
56
|
+
version: @config.version,
|
57
|
+
instance_id: @config.instance_id,
|
58
|
+
published_subjects: @config.subject_patterns,
|
59
|
+
timestamp: Time.current.iso8601,
|
60
|
+
}
|
61
|
+
end
|
62
|
+
|
63
|
+
def connected?
|
64
|
+
@nats_client && @nats_client.connected?
|
65
|
+
end
|
66
|
+
|
67
|
+
def disconnect!
|
68
|
+
@subscriber&.unsubscribe_all
|
69
|
+
@nats_client&.close
|
70
|
+
@connection_pool&.shutdown
|
71
|
+
@connection_pool&.wait_for_termination(5)
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
def setup_connection_pool
|
77
|
+
return unless defined?(Concurrent)
|
78
|
+
|
79
|
+
@connection_pool = Concurrent::ThreadPoolExecutor.new(
|
80
|
+
min_threads: 2,
|
81
|
+
max_threads: @config.connection_pool_size
|
82
|
+
)
|
83
|
+
end
|
84
|
+
|
85
|
+
def setup_middleware
|
86
|
+
@middleware_stack = []
|
87
|
+
|
88
|
+
if @config.middleware_authentication_enabled
|
89
|
+
@middleware_stack << Middleware::Authentication.new(@config)
|
90
|
+
end
|
91
|
+
|
92
|
+
if @config.middleware_validation_enabled
|
93
|
+
@middleware_stack << Middleware::Validation.new(@config)
|
94
|
+
end
|
95
|
+
|
96
|
+
if @config.middleware_logging_enabled
|
97
|
+
@middleware_stack << Middleware::Logging.new(@config)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def establish_connections
|
102
|
+
return unless nats_available?
|
103
|
+
|
104
|
+
establish_nats_connection
|
105
|
+
|
106
|
+
if @config.publishing_enabled
|
107
|
+
@publisher = Publisher.new(@config, @nats_client, @middleware_stack)
|
108
|
+
end
|
109
|
+
|
110
|
+
if @config.subscription_enabled
|
111
|
+
@subscriber = Subscriber.new(@config, @nats_client, @middleware_stack)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def establish_nats_connection
|
116
|
+
@nats_client = NATS.connect(
|
117
|
+
@config.nats_url,
|
118
|
+
reconnect_time_wait: @config.retry_delay,
|
119
|
+
max_reconnect_attempts: @config.reconnect_attempts
|
120
|
+
)
|
121
|
+
NatsWave.logger.info("Connected to NATS at #{@config.nats_url}")
|
122
|
+
rescue => e
|
123
|
+
NatsWave.logger.error("Failed to connect to NATS: #{e.message}")
|
124
|
+
raise ConnectionError, "Failed to connect to NATS: #{e.message}"
|
125
|
+
end
|
126
|
+
|
127
|
+
def ensure_connected!
|
128
|
+
raise ConnectionError, "Not connected to NATS" unless connected?
|
129
|
+
end
|
130
|
+
|
131
|
+
def database_connected?
|
132
|
+
return false unless @subscriber
|
133
|
+
@subscriber.database_connected?
|
134
|
+
end
|
135
|
+
|
136
|
+
def nats_available?
|
137
|
+
defined?(NATS)
|
138
|
+
rescue LoadError
|
139
|
+
false
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
@@ -0,0 +1,172 @@
|
|
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
|
+
mapping = {
|
26
|
+
field_mappings: options[:field_mappings] || {},
|
27
|
+
transformations: options[:transformations] || {},
|
28
|
+
conditions: options[:conditions] || {},
|
29
|
+
sync_strategy: options[:sync_strategy] || :upsert,
|
30
|
+
unique_fields: options[:unique_fields] || [:id],
|
31
|
+
skip_fields: options[:skip_fields] || [],
|
32
|
+
# NEW: Subscription configuration
|
33
|
+
subjects: options[:subjects] || [],
|
34
|
+
handler: options[:handler],
|
35
|
+
queue_group: options[:queue_group]
|
36
|
+
}
|
37
|
+
|
38
|
+
self.nats_wave_mapping_config[external_model] = mapping
|
39
|
+
|
40
|
+
# Register this mapping globally
|
41
|
+
NatsWave::ModelRegistry.register_reverse_mapping(external_model, self.name, mapping)
|
42
|
+
|
43
|
+
# Register subscription if subjects are provided
|
44
|
+
if mapping[:subjects].any?
|
45
|
+
NatsWave::ModelRegistry.register_subscription(
|
46
|
+
subjects: mapping[:subjects],
|
47
|
+
model: self.name,
|
48
|
+
external_model: external_model,
|
49
|
+
handler: mapping[:handler],
|
50
|
+
queue_group: mapping[:queue_group]
|
51
|
+
)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# Configure subscriptions without model mapping (for custom handlers)
|
56
|
+
def nats_wave_subscribes_to(*subjects, handler: nil, queue_group: nil, &block)
|
57
|
+
handler ||= block
|
58
|
+
|
59
|
+
subscription_config = {
|
60
|
+
subjects: subjects.flatten,
|
61
|
+
handler: handler,
|
62
|
+
queue_group: queue_group,
|
63
|
+
model: self.name
|
64
|
+
}
|
65
|
+
|
66
|
+
self.nats_wave_subscription_config[:custom] = subscription_config
|
67
|
+
|
68
|
+
# Register subscription
|
69
|
+
NatsWave::ModelRegistry.register_subscription(
|
70
|
+
subjects: subjects.flatten,
|
71
|
+
model: self.name,
|
72
|
+
handler: handler,
|
73
|
+
queue_group: queue_group
|
74
|
+
)
|
75
|
+
end
|
76
|
+
|
77
|
+
# Auto-generate subjects based on external model pattern
|
78
|
+
def nats_wave_auto_subjects_for(external_model, service_prefix: nil, actions: [:create, :update, :destroy])
|
79
|
+
model_name = external_model.underscore
|
80
|
+
|
81
|
+
if service_prefix
|
82
|
+
subjects = actions.map { |action| "#{service_prefix}.#{model_name}.#{action}" }
|
83
|
+
subjects << "#{service_prefix}.#{model_name}.*" # Wildcard for all actions
|
84
|
+
else
|
85
|
+
# Try to infer from common patterns
|
86
|
+
subjects = [
|
87
|
+
"#{model_name}.*", # Simple pattern
|
88
|
+
"events.#{model_name}.*", # Events pattern
|
89
|
+
"*.#{model_name}.*", # Service.model pattern
|
90
|
+
"*.events.#{model_name}.*" # Service.events.model pattern
|
91
|
+
]
|
92
|
+
end
|
93
|
+
|
94
|
+
subjects
|
95
|
+
end
|
96
|
+
|
97
|
+
# Get mapping configuration for an external model
|
98
|
+
def nats_wave_mapping_for(external_model)
|
99
|
+
nats_wave_mapping_config[external_model]
|
100
|
+
end
|
101
|
+
|
102
|
+
# Check if this model can sync from an external model
|
103
|
+
def nats_wave_can_sync_from?(external_model)
|
104
|
+
nats_wave_mapping_config.key?(external_model)
|
105
|
+
end
|
106
|
+
|
107
|
+
# Get all external models this model can sync from
|
108
|
+
def nats_wave_external_models
|
109
|
+
nats_wave_mapping_config.keys
|
110
|
+
end
|
111
|
+
|
112
|
+
# Get all subjects this model subscribes to
|
113
|
+
def nats_wave_subscribed_subjects
|
114
|
+
subjects = []
|
115
|
+
|
116
|
+
# From model mappings
|
117
|
+
nats_wave_mapping_config.each do |_, config|
|
118
|
+
subjects.concat(config[:subjects] || [])
|
119
|
+
end
|
120
|
+
|
121
|
+
# From custom subscriptions
|
122
|
+
if nats_wave_subscription_config[:custom]
|
123
|
+
subjects.concat(nats_wave_subscription_config[:custom][:subjects] || [])
|
124
|
+
end
|
125
|
+
|
126
|
+
subjects.uniq
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
# Instance methods remain the same...
|
131
|
+
def nats_wave_mapped_attributes_for(target_model)
|
132
|
+
mapping = self.class.nats_wave_mapping_config[target_model]
|
133
|
+
return attributes unless mapping
|
134
|
+
|
135
|
+
mapped_attrs = {}
|
136
|
+
field_mappings = mapping[:field_mappings] || {}
|
137
|
+
skip_fields = mapping[:skip_fields] || []
|
138
|
+
|
139
|
+
attributes.each do |key, value|
|
140
|
+
next if skip_fields.include?(key.to_s) || skip_fields.include?(key.to_sym)
|
141
|
+
|
142
|
+
mapped_key = field_mappings[key] || field_mappings[key.to_sym] || key
|
143
|
+
mapped_attrs[mapped_key] = transform_value(value, mapped_key, mapping[:transformations] || {})
|
144
|
+
end
|
145
|
+
|
146
|
+
mapped_attrs
|
147
|
+
end
|
148
|
+
|
149
|
+
def nats_wave_unique_identifier_for(external_model)
|
150
|
+
mapping = self.class.nats_wave_mapping_config[external_model]
|
151
|
+
unique_fields = mapping&.dig(:unique_fields) || [:id]
|
152
|
+
|
153
|
+
unique_fields.map { |field| [field, send(field)] }.to_h
|
154
|
+
end
|
155
|
+
|
156
|
+
private
|
157
|
+
|
158
|
+
def transform_value(value, field, transformations)
|
159
|
+
transformation = transformations[field] || transformations[field.to_sym]
|
160
|
+
|
161
|
+
case transformation
|
162
|
+
when Proc
|
163
|
+
transformation.call(value)
|
164
|
+
when Symbol
|
165
|
+
send(transformation, value) if respond_to?(transformation, true)
|
166
|
+
else
|
167
|
+
value
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|