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
data/Rakefile
ADDED
@@ -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
|