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.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/.idea/.gitignore +8 -0
  3. data/.idea/misc.xml +4 -0
  4. data/.idea/modules.xml +8 -0
  5. data/.idea/nats_wave.iml +169 -0
  6. data/.idea/vcs.xml +6 -0
  7. data/.rspec +3 -0
  8. data/.rubocop.yml +16 -0
  9. data/CHANGELOG.md +5 -0
  10. data/CODE_OF_CONDUCT.md +136 -0
  11. data/Gemfile +15 -0
  12. data/Gemfile.lock +332 -0
  13. data/LICENSE.txt +21 -0
  14. data/README.md +985 -0
  15. data/Rakefile +12 -0
  16. data/config/nats_wave.yml +65 -0
  17. data/examples/catalog_model.rb +36 -0
  18. data/examples/configuration_examples.rb +68 -0
  19. data/examples/user_model.rb +58 -0
  20. data/lib/generators/nats_wave/install_generator.rb +40 -0
  21. data/lib/generators/nats_wave/templates/README +31 -0
  22. data/lib/generators/nats_wave/templates/create_nats_wave_failed_messages.rb +20 -0
  23. data/lib/generators/nats_wave/templates/create_nats_wave_failed_subscriptions.rb +20 -0
  24. data/lib/generators/nats_wave/templates/initializer.rb +64 -0
  25. data/lib/generators/nats_wave/templates/nats_wave.yml +65 -0
  26. data/lib/nats_wave/adapters/active_record.rb +206 -0
  27. data/lib/nats_wave/adapters/datadog_metrics.rb +93 -0
  28. data/lib/nats_wave/auto_registration.rb +109 -0
  29. data/lib/nats_wave/client.rb +142 -0
  30. data/lib/nats_wave/concerns/mappable.rb +172 -0
  31. data/lib/nats_wave/concerns/publishable.rb +216 -0
  32. data/lib/nats_wave/configuration.rb +105 -0
  33. data/lib/nats_wave/database_connector.rb +50 -0
  34. data/lib/nats_wave/dead_letter_queue.rb +146 -0
  35. data/lib/nats_wave/errors.rb +27 -0
  36. data/lib/nats_wave/message_transformer.rb +95 -0
  37. data/lib/nats_wave/metrics.rb +220 -0
  38. data/lib/nats_wave/middleware/authentication.rb +65 -0
  39. data/lib/nats_wave/middleware/base.rb +19 -0
  40. data/lib/nats_wave/middleware/logging.rb +58 -0
  41. data/lib/nats_wave/middleware/validation.rb +74 -0
  42. data/lib/nats_wave/model_mapper.rb +125 -0
  43. data/lib/nats_wave/model_registry.rb +150 -0
  44. data/lib/nats_wave/publisher.rb +151 -0
  45. data/lib/nats_wave/railtie.rb +111 -0
  46. data/lib/nats_wave/schema_registry.rb +77 -0
  47. data/lib/nats_wave/subscriber.rb +161 -0
  48. data/lib/nats_wave/version.rb +5 -0
  49. data/lib/nats_wave.rb +97 -0
  50. data/lib/tasks/nats_wave.rake +360 -0
  51. data/sig/nats_wave.rbs +5 -0
  52. metadata +385 -0
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require 'rubocop/rake_task'
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
@@ -0,0 +1,65 @@
1
+ default: &default
2
+ nats:
3
+ url: <%= ENV['NATS_URL'] %>
4
+ connection_pool_size: 10
5
+ timeout: 5
6
+ reconnect_attempts: 3
7
+
8
+ publishing:
9
+ enabled: true
10
+ default_subject_prefix: <%= ENV['SERVICE_NAME'] %>
11
+ batch_size: 100
12
+ async: true
13
+
14
+ subscription:
15
+ enabled: true
16
+ subjects: [ ]
17
+ queue_group: <%= ENV['SERVICE_NAME'] %>_consumers
18
+
19
+ database:
20
+ adapter: "active_record"
21
+ connection_pool_size: 5
22
+
23
+ # middleware:
24
+ # authentication:
25
+ # enabled: false
26
+ # secret_key: <%= ENV['NATS_AUTH_SECRET'] %>
27
+ # validation:
28
+ # enabled: true
29
+ # schema_registry: <%= ENV['SCHEMA_REGISTRY_URL'] %>
30
+ # logging:
31
+ # enabled: true
32
+ # level: "info"
33
+
34
+ error_handling:
35
+ max_retries: 3
36
+ retry_delay: 5
37
+ dead_letter_queue: "failed_messages"
38
+
39
+ development:
40
+ <<: *default
41
+ middleware:
42
+ authentication:
43
+ enabled: false
44
+ logging:
45
+ level: "debug"
46
+
47
+ test:
48
+ <<: *default
49
+ nats:
50
+ url: <%= ENV['NATS_URL'] %>
51
+ publishing:
52
+ enabled: false
53
+ subscription:
54
+ enabled: false
55
+
56
+ production:
57
+ <<: *default
58
+ nats:
59
+ url: <%= ENV['NATS_URL'] %>
60
+ middleware:
61
+ authentication:
62
+ enabled: true
63
+ secret_key: <%= ENV['NATS_AUTH_SECRET'] %>
64
+ logging:
65
+ level: "info"
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Catalog < ApplicationRecord
4
+ include NatsWave::NatsPublishable
5
+
6
+ nats_publishable(
7
+ actions: %i[create update], # Don't publish deletes
8
+ skip_attributes: %i[view_count search_vector internal_notes],
9
+ include_associations: %i[category variants]
10
+ )
11
+
12
+ # Custom serializer support
13
+ def self.nats_wave_attribute_transformations
14
+ {
15
+ hammer_estimate: ->(value, _action) { value.to_f.round(2) },
16
+ name: :sanitize_name,
17
+ description: lambda { |value, _action|
18
+ ActionView::Base.full_sanitizer.sanitize(value)
19
+ }
20
+ }
21
+ end
22
+
23
+ private
24
+
25
+ def sanitize_name(name, _action)
26
+ name&.strip&.titleize
27
+ end
28
+
29
+ def nats_wave_metadata
30
+ {
31
+ organization: organization&.name,
32
+ make:,
33
+ catalog_tracked: track_catalog?
34
+ }
35
+ end
36
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Basic configuration
4
+ NatsWave.configure do |config|
5
+ config.nats_url = 'nats://localhost:4222'
6
+ config.service_name = 'ecommerce_api'
7
+ config.default_subject_prefix = 'ecommerce.events'
8
+ end
9
+
10
+ # Advanced configuration with model mappings
11
+ NatsWave.configure do |config|
12
+ config.nats_url = ENV.fetch('NATS_URL', 'nats://localhost:4222')
13
+ config.service_name = 'catalog_service'
14
+ config.environment = Rails.env
15
+
16
+ # Subscribe to other team's events
17
+ config.add_subscription('warehouse.assets.*')
18
+ config.add_subscription('external.catalogs.*')
19
+ config.add_subscription('payments.transactions.*')
20
+
21
+ # Map external models to local models
22
+ config.add_model_mapping('WarehouseAsset', 'Catalog', {
23
+ 'asset_name' => 'name',
24
+ 'asset_description' => 'description',
25
+ 'asset_value' => 'hammer_estimate',
26
+ 'asset_id' => 'external_icn',
27
+ 'model' => 'category'
28
+ })
29
+
30
+ config.add_model_mapping('ExternalCatalog', 'Catalog', {
31
+ 'package_name' => 'name',
32
+ 'package_cost' => 'hammer_estimate',
33
+ 'external_vin' => 'vin'
34
+ })
35
+
36
+ # Custom transformation rules
37
+ config.transformation_rules = {
38
+ 'hammer_estimate' => ->(value) { value.to_f.round(2) },
39
+ 'name' => ->(value) { value&.titleize },
40
+ 'email' => ->(value) { value&.downcase }
41
+ }
42
+
43
+ # Custom subscription with handler
44
+ config.add_subscription('payments.failed.*') do |message|
45
+ PaymentFailureHandler.process(message)
46
+ end
47
+
48
+ # Middleware configuration
49
+ config.middleware_authentication_enabled = Rails.env.production?
50
+ config.auth_secret_key = ENV['NATS_AUTH_SECRET']
51
+ config.middleware_validation_enabled = true
52
+ config.middleware_logging_enabled = true
53
+ config.log_level = Rails.env.production? ? 'info' : 'debug'
54
+ end
55
+
56
+ # Catalogion configuration
57
+ if Rails.env.production?
58
+ NatsWave.configure do |config|
59
+ config.nats_url = ENV.fetch('NATS_CLUSTER_URL')
60
+ config.connection_pool_size = 20
61
+ config.async_publishing = true
62
+ config.middleware_authentication_enabled = true
63
+ config.auth_secret_key = ENV.fetch('NATS_AUTH_SECRET')
64
+ config.schema_registry_url = ENV['SCHEMA_REGISTRY_URL']
65
+ config.max_retries = 5
66
+ config.retry_delay = 10
67
+ end
68
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ class User < ApplicationRecord
4
+ include NatsWave::NatsPublishable
5
+
6
+ # Configure NATS publishing
7
+ nats_publishable(
8
+ actions: %i[create update destroy],
9
+ skip_attributes: %i[password_digest remember_token password_reset_token],
10
+ include_associations: %i[profile organization],
11
+ subject_prefix: 'users',
12
+ metadata: { tenant_id: 'main' },
13
+ if: -> { active? },
14
+ unless: -> { system_user? }
15
+ )
16
+
17
+ # Define unique attributes for syncing
18
+ def self.nats_wave_unique_attributes
19
+ %i[email external_icn username]
20
+ end
21
+
22
+ # Define attributes that shouldn't be synced from other instances
23
+ def self.nats_wave_skip_sync_attributes
24
+ %i[last_login_at login_count password_digest]
25
+ end
26
+
27
+ # Custom attribute transformations
28
+ def self.nats_wave_attribute_transformations
29
+ {
30
+ email: ->(value, _action) { value&.downcase&.strip },
31
+ phone: :format_phone_number
32
+ }
33
+ end
34
+
35
+ private
36
+
37
+ # Custom metadata for NATS messages
38
+ def nats_wave_metadata
39
+ {
40
+ tenant_id: organization&.tenant_id,
41
+ user_type: account_type,
42
+ subscription_level: subscription&.level
43
+ }
44
+ end
45
+
46
+ def format_phone_number(phone, _action)
47
+ # Custom transformation logic
48
+ phone&.gsub(/\D/, '')
49
+ end
50
+
51
+ def active?
52
+ status == 'active'
53
+ end
54
+
55
+ def system_user?
56
+ email&.ends_with?('@system.internal')
57
+ end
58
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+ require 'rails/generators/migration'
5
+
6
+ module NatsWave
7
+ module Generators
8
+ class InstallGenerator < Rails::Generators::Base
9
+ include Rails::Generators::Migration
10
+
11
+ source_root File.expand_path('templates', __dir__)
12
+
13
+ def self.next_migration_number(path)
14
+ next_migration_number = current_migration_number(path) + 1
15
+ ActiveRecord::Migration.next_migration_number(next_migration_number)
16
+ end
17
+
18
+ desc 'Creates a NatsWave initializer and configuration file'
19
+
20
+ def copy_initializer
21
+ template 'initializer.rb', 'config/initializers/nats_wave.rb'
22
+ end
23
+
24
+ def copy_configuration
25
+ template 'nats_wave.yml', 'config/nats_wave.yml'
26
+ end
27
+
28
+ def create_migrations
29
+ migration_template 'create_nats_wave_failed_messages.rb',
30
+ 'db/migrate/create_nats_wave_failed_messages.rb'
31
+ migration_template 'create_nats_wave_failed_subscriptions.rb',
32
+ 'db/migrate/create_nats_wave_failed_subscriptions.rb'
33
+ end
34
+
35
+ def show_instructions
36
+ readme 'README' if behavior == :invoke
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,31 @@
1
+ ===============================================================================
2
+
3
+ NatsWave has been installed successfully!
4
+
5
+ Next steps:
6
+
7
+ 1. Run the migrations:
8
+ rails db:migrate
9
+
10
+ 2. Configure your models:
11
+
12
+ class User < ApplicationRecord
13
+ nats_publishable(
14
+ actions: [:create, :update, :destroy],
15
+ skip_attributes: [:password_digest, :remember_token],
16
+ include_associations: [:profile]
17
+ )
18
+ end
19
+
20
+ 3. Start the subscriber:
21
+ rails nats_wave:start
22
+
23
+ 4. Test the health:
24
+ rails nats_wave:health
25
+
26
+ 5. Configure your NATS server URL:
27
+ export NATS_URL=nats://your-nats-server:4222
28
+
29
+ For more information, visit: https://github.com/PurpleWave/nats_wave
30
+
31
+ ===============================================================================
@@ -0,0 +1,20 @@
1
+ class CreateNatsWaveFailedMessages < ActiveRecord::Migration[6.1]
2
+ def change
3
+ create_table :nats_wave_failed_messages do |t|
4
+ t.string :subject, null: false
5
+ t.text :payload, null: false
6
+ t.text :error_message, null: false
7
+ t.text :error_backtrace
8
+ t.string :error_class
9
+ t.integer :retry_count, default: 0, null: false
10
+ t.datetime :next_retry_at
11
+ t.datetime :last_retry_at
12
+ t.timestamps
13
+ end
14
+
15
+ add_index :nats_wave_failed_messages, :subject
16
+ add_index :nats_wave_failed_messages, [:retry_count, :next_retry_at], name: 'idx_nats_wave_failed_retry'
17
+ add_index :nats_wave_failed_messages, :created_at
18
+ add_index :nats_wave_failed_messages, :error_class
19
+ end
20
+ end
@@ -0,0 +1,20 @@
1
+ class CreateNatsWaveFailedSubscriptions < ActiveRecord::Migration[6.1]
2
+ def change
3
+ create_table :nats_wave_failed_subscriptions do |t|
4
+ t.string :subject, null: false
5
+ t.text :message, null: false
6
+ t.text :error_message, null: false
7
+ t.text :error_backtrace
8
+ t.string :error_class
9
+ t.integer :retry_count, default: 0, null: false
10
+ t.datetime :next_retry_at
11
+ t.datetime :last_retry_at
12
+ t.timestamps
13
+ end
14
+
15
+ add_index :nats_wave_failed_subscriptions, :subject
16
+ add_index :nats_wave_failed_subscriptions, [:retry_count, :next_retry_at], name: 'idx_nats_wave_subs_retry'
17
+ add_index :nats_wave_failed_subscriptions, :created_at
18
+ add_index :nats_wave_failed_subscriptions, :error_class
19
+ end
20
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ NatsWave.configure do |config|
4
+ # Basic configuration
5
+ config.nats_url = ENV.fetch('NATS_URL', 'nats://localhost:4222')
6
+ config.service_name = ENV.fetch('SERVICE_NAME',
7
+ Rails.application.class.name.deconstantize.underscore)
8
+ config.environment = Rails.env
9
+
10
+ # Publishing configuration
11
+ config.default_subject_prefix = "#{config.service_name}.events"
12
+ config.publishing_enabled = true
13
+ config.async_publishing = true
14
+
15
+ # Subscription configuration
16
+ config.subscription_enabled = true
17
+ config.queue_group = "#{config.service_name}_consumers"
18
+
19
+ # Model mappings (example)
20
+ # config.add_model_mapping('ExternalAsset', 'Catalog', {
21
+ # 'name' => 'title',
22
+ # 'description' => 'body',
23
+ # 'hammer_estimate' => 'cost'
24
+ # })
25
+
26
+ # Subscriptions (example)
27
+ # config.add_subscription('external.assets.*')
28
+ # config.add_subscription('catalog.items.*')
29
+
30
+ # Custom subscription with handler
31
+ # config.add_subscription('webhooks.external.*') do |message|
32
+ # WebhookProcessor.process(message)
33
+ # end
34
+
35
+ # Middleware
36
+ config.middleware_logging_enabled = true
37
+ config.log_level = Rails.env.production? ? 'info' : 'debug'
38
+
39
+ # Authentication (optional)
40
+ # config.middleware_authentication_enabled = true
41
+ # config.auth_secret_key = ENV['NATS_AUTH_SECRET']
42
+
43
+ # Validation (optional)
44
+ # config.middleware_validation_enabled = true
45
+ # config.schema_registry_url = ENV['SCHEMA_REGISTRY_URL']
46
+ end
47
+
48
+ # Configure metrics backend if available
49
+ NatsWave::Metrics.metrics_backend = StatsD if defined?(StatsD)
50
+
51
+ # Auto-start subscriber in development
52
+ if Rails.env.development?
53
+ Rails.application.config.after_initialize do
54
+ Thread.new do
55
+ sleep 2 # Give Rails time to boot
56
+ begin
57
+ NatsWave.start_subscriber
58
+ Rails.logger.info 'NatsWave subscriber started automatically'
59
+ rescue StandardError => e
60
+ Rails.logger.error "Failed to start NatsWave subscriber: #{e.message}"
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,65 @@
1
+ default: &default
2
+ nats:
3
+ url: <%= ENV['NATS_URL'] %>
4
+ connection_pool_size: 10
5
+ timeout: 5
6
+ reconnect_attempts: 3
7
+
8
+ publishing:
9
+ enabled: true
10
+ default_subject_prefix: <%= ENV['SERVICE_NAME'] %>
11
+ batch_size: 100
12
+ async: true
13
+
14
+ subscription:
15
+ enabled: true
16
+ subjects: [ ]
17
+ queue_group: <%= ENV['SERVICE_NAME'] %>_consumers
18
+
19
+ database:
20
+ adapter: "active_record"
21
+ connection_pool_size: 5
22
+
23
+ # middleware:
24
+ # authentication:
25
+ # enabled: false
26
+ # secret_key: <%= ENV['NATS_AUTH_SECRET'] %>
27
+ # validation:
28
+ # enabled: true
29
+ # schema_registry: <%= ENV['SCHEMA_REGISTRY_URL'] %>
30
+ # logging:
31
+ # enabled: true
32
+ # level: "info"
33
+
34
+ error_handling:
35
+ max_retries: 3
36
+ retry_delay: 5
37
+ dead_letter_queue: "failed_messages"
38
+
39
+ development:
40
+ <<: *default
41
+ middleware:
42
+ authentication:
43
+ enabled: false
44
+ logging:
45
+ level: "debug"
46
+
47
+ test:
48
+ <<: *default
49
+ nats:
50
+ url: <%= ENV['NATS_URL'] %>
51
+ publishing:
52
+ enabled: false
53
+ subscription:
54
+ enabled: false
55
+
56
+ production:
57
+ <<: *default
58
+ nats:
59
+ url: <%= ENV['NATS_URL'] %>
60
+ middleware:
61
+ authentication:
62
+ enabled: true
63
+ secret_key: <%= ENV['NATS_AUTH_SECRET'] %>
64
+ logging:
65
+ level: "info"
@@ -0,0 +1,206 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NatsWave
4
+ module Adapters
5
+ class ActiveRecord
6
+ def initialize(config)
7
+ @config = config
8
+ end
9
+
10
+ def create_from_external(external_model, data, metadata = {})
11
+ mapping = ModelRegistry.local_models_for(external_model)
12
+
13
+ mapping.each do |local_model_name, config|
14
+ create_or_update_local_record(local_model_name, external_model, data, config, 'create')
15
+ end
16
+ end
17
+
18
+ def update_from_external(external_model, data, metadata = {})
19
+ mapping = ModelRegistry.local_models_for(external_model)
20
+
21
+ mapping.each do |local_model_name, config|
22
+ create_or_update_local_record(local_model_name, external_model, data, config, 'update')
23
+ end
24
+ end
25
+
26
+ def destroy_from_external(external_model, data, metadata = {})
27
+ mapping = ModelRegistry.local_models_for(external_model)
28
+
29
+ mapping.each do |local_model_name, config|
30
+ destroy_local_record(local_model_name, external_model, data, config)
31
+ end
32
+ end
33
+
34
+ # Legacy methods for backward compatibility
35
+ def create(model_name, data, metadata = {})
36
+ model_class = get_model_class(model_name)
37
+ clean_data = data.except('created_at', 'updated_at', :created_at, :updated_at)
38
+
39
+ with_nats_publishing_disabled do
40
+ record = model_class.create!(clean_data)
41
+ NatsWave.logger.debug("Created #{model_name} with ID: #{record.id}")
42
+ record
43
+ end
44
+ rescue => e
45
+ raise DatabaseError, "Failed to create #{model_name}: #{e.message}"
46
+ end
47
+
48
+ def connected?
49
+ return false unless defined?(::ActiveRecord)
50
+ ::ActiveRecord::Base.connected?
51
+ rescue
52
+ false
53
+ end
54
+
55
+ private
56
+
57
+ def create_or_update_local_record(local_model_name, external_model, external_data, config, action)
58
+ model_class = get_model_class(local_model_name)
59
+
60
+ # Transform the data using field mappings and transformations
61
+ transformed_data = transform_external_data(external_data, config)
62
+
63
+ # Find existing record based on unique fields
64
+ existing_record = find_existing_record(model_class, transformed_data, config[:unique_fields])
65
+
66
+ with_nats_publishing_disabled do
67
+ case config[:sync_strategy]
68
+ when :create_only
69
+ create_record_if_not_exists(model_class, existing_record, transformed_data)
70
+ when :update_only
71
+ update_record_if_exists(existing_record, transformed_data) if existing_record
72
+ when :upsert, nil
73
+ upsert_record(model_class, existing_record, transformed_data, config)
74
+ end
75
+ end
76
+
77
+ # Track the sync operation
78
+ NatsWave::Metrics.track_sync_operation(
79
+ local_model_name,
80
+ action,
81
+ 0, # duration - you could track this
82
+ success: true
83
+ )
84
+
85
+ rescue => e
86
+ NatsWave.logger.error("Failed to sync #{external_model} to #{local_model_name}: #{e.message}")
87
+
88
+ NatsWave::Metrics.track_sync_operation(
89
+ local_model_name,
90
+ action,
91
+ 0,
92
+ success: false
93
+ )
94
+
95
+ raise DatabaseError, "Sync failed for #{local_model_name}: #{e.message}"
96
+ end
97
+
98
+ def transform_external_data(external_data, config)
99
+ field_mappings = config[:field_mappings] || {}
100
+ transformations = config[:transformations] || {}
101
+ skip_fields = config[:skip_fields] || []
102
+
103
+ transformed = {}
104
+
105
+ external_data.each do |key, value|
106
+ next if skip_fields.include?(key.to_s) || skip_fields.include?(key.to_sym)
107
+
108
+ # Map field name
109
+ local_field = field_mappings[key] || field_mappings[key.to_sym] || key
110
+
111
+ # Apply transformation
112
+ if transformations[local_field]
113
+ transformation = transformations[local_field]
114
+ value = case transformation
115
+ when Proc
116
+ transformation.arity == 1 ? transformation.call(value) : transformation.call(value, external_data)
117
+ when Symbol
118
+ # Would need to be called on the model instance
119
+ value
120
+ else
121
+ value
122
+ end
123
+ end
124
+
125
+ transformed[local_field] = value
126
+ end
127
+
128
+ # Remove Rails timestamp fields that might conflict
129
+ transformed.except('created_at', 'updated_at', :created_at, :updated_at)
130
+ end
131
+
132
+ def find_existing_record(model_class, data, unique_fields)
133
+ unique_fields ||= [:id]
134
+
135
+ unique_fields.each do |field|
136
+ value = data[field] || data[field.to_sym]
137
+ next unless value
138
+
139
+ record = model_class.find_by(field => value)
140
+ return record if record
141
+ end
142
+
143
+ nil
144
+ end
145
+
146
+ def create_record_if_not_exists(model_class, existing_record, data)
147
+ return existing_record if existing_record
148
+
149
+ record = model_class.create!(data)
150
+ NatsWave.logger.info("Created #{model_class.name} with ID: #{record.id}")
151
+ record
152
+ end
153
+
154
+ def update_record_if_exists(existing_record, data)
155
+ return unless existing_record
156
+
157
+ existing_record.update!(data)
158
+ NatsWave.logger.info("Updated #{existing_record.class.name} ID: #{existing_record.id}")
159
+ existing_record
160
+ end
161
+
162
+ def upsert_record(model_class, existing_record, data, config)
163
+ if existing_record
164
+ existing_record.update!(data)
165
+ NatsWave.logger.info("Updated #{model_class.name} ID: #{existing_record.id}")
166
+ existing_record
167
+ else
168
+ record = model_class.create!(data)
169
+ NatsWave.logger.info("Created #{model_class.name} with ID: #{record.id}")
170
+ record
171
+ end
172
+ end
173
+
174
+ def destroy_local_record(local_model_name, external_model, external_data, config)
175
+ model_class = get_model_class(local_model_name)
176
+ transformed_data = transform_external_data(external_data, config)
177
+
178
+ existing_record = find_existing_record(model_class, transformed_data, config[:unique_fields])
179
+ return unless existing_record
180
+
181
+ with_nats_publishing_disabled do
182
+ if existing_record.respond_to?(:soft_delete)
183
+ existing_record.soft_delete
184
+ else
185
+ existing_record.destroy!
186
+ end
187
+
188
+ NatsWave.logger.info("Destroyed #{local_model_name} ID: #{existing_record.id}")
189
+ end
190
+ end
191
+
192
+ def get_model_class(model_name)
193
+ model_name.constantize
194
+ rescue NameError
195
+ raise DatabaseError, "Model class '#{model_name}' not found"
196
+ end
197
+
198
+ def with_nats_publishing_disabled(&block)
199
+ Thread.current[:skip_nats_wave_publishing] = true
200
+ yield
201
+ ensure
202
+ Thread.current[:skip_nats_wave_publishing] = false
203
+ end
204
+ end
205
+ end
206
+ end