jetstream_bridge 1.7.0 → 1.8.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0e0d3ee00b372ffe54ceca93b81a7c573385f6b85f16f584f15e8c5eef82cd44
4
- data.tar.gz: b56fa1bc39e3b598719bced5b0be60172cd953214bee399c00341fd1f2d00906
3
+ metadata.gz: 4b7171c9e1932f55d61a904c1e372b65ba56d2fa0eced6f72ac42b6d4f3f7020
4
+ data.tar.gz: 8f6defde8bdf18e5421401a5b357c12b4d958efeaa91bf4963d4e253e29551ef
5
5
  SHA512:
6
- metadata.gz: 342b6b3431d3768b843060402ededa4dcca39100c1d0c005153a3ea4f758a9ab6aff9a09e5d608444acbb332766cfb926f3c1555afb8d9df882cfcdb7a8b405f
7
- data.tar.gz: eb257f387a461444121cb304c36e9fa3ce13062c81a8bc43c0cda4e6f6c706c2efa8cc7a4cf3fdfc721cedeab1d11f0ca8bb00e943644c01575d051f5e143c19
6
+ metadata.gz: 798ccab4db891332fd08acfa888e400a06b49d79e1ce835729f59536e365fb953cb12ace04e3f55973823716c0b8b955245f0f7a6dd5c1895a35cd403eb8ee76
7
+ data.tar.gz: 36c5b015c44a74213c11bd5bcd769dfd97409b4c022b9c3c569d587ff2c356a9b7eb59c36772be9601fe11f7a8a8fa4551f36a44a5ab4046f07f129c87b0c7b8
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- jetstream_bridge (1.7.0)
4
+ jetstream_bridge (1.8.0)
5
5
  activerecord (>= 6.0)
6
6
  activesupport (>= 6.0)
7
7
  nats-pure (~> 2.4)
@@ -142,8 +142,6 @@ GEM
142
142
  racc (~> 1.4)
143
143
  nokogiri (1.18.7-arm64-darwin)
144
144
  racc (~> 1.4)
145
- nokogiri (1.18.7-x86_64-linux-gnu)
146
- racc (~> 1.4)
147
145
  parallel (1.27.0)
148
146
  parser (3.3.9.0)
149
147
  ast (~> 2.4.1)
@@ -3,62 +3,59 @@
3
3
  begin
4
4
  require 'active_record'
5
5
  rescue LoadError
6
- # No-op; shim below if AR missing.
6
+ # No-op; shim defined below.
7
7
  end
8
8
 
9
9
  module JetstreamBridge
10
- # Default Inbox model when `use_inbox` is enabled.
11
- # Prefers event_id, but can fall back to (stream, stream_seq).
12
10
  if defined?(ActiveRecord::Base)
13
11
  class InboxEvent < ActiveRecord::Base
14
12
  self.table_name = 'jetstream_inbox_events'
15
13
 
16
14
  class << self
17
- def column?(name) = column_names.include?(name.to_s)
15
+ def has_column?(name)
16
+ return false unless ar_connected?
18
17
 
19
- def attribute_json?(name)
20
- return false unless respond_to?(:attribute_types) && attribute_types.key?(name.to_s)
21
- attribute_types[name.to_s].to_s.downcase.include?('json')
18
+ connection.schema_cache.columns_hash(table_name).key?(name.to_s)
19
+ rescue ActiveRecord::ConnectionNotEstablished, ActiveRecord::NoDatabaseError
20
+ false
22
21
  end
23
- end
24
22
 
25
- if column?(:payload)
26
- serialize :payload, coder: JSON unless attribute_json?(:payload)
27
- end
28
- if column?(:headers)
29
- serialize :headers, coder: JSON unless attribute_json?(:headers)
23
+ def ar_connected?
24
+ ActiveRecord::Base.connected? && connection_pool.active_connection?
25
+ rescue StandardError
26
+ false
27
+ end
30
28
  end
31
29
 
32
- if column?(:event_id)
33
- validates :event_id, presence: true, uniqueness: true
34
- elsif column?(:stream_seq)
35
- validates :stream_seq, presence: true
36
- validates :stream, presence: true, if: -> { self.class.column?(:stream) }
37
- if column?(:stream)
30
+ # Validations guarded by safe checks
31
+ if_condition = -> { self.class.has_column?(:event_id) }
32
+ unless_condition = -> { !self.class.has_column?(:event_id) }
33
+
34
+ validates :event_id, presence: true, uniqueness: true, if: if_condition
35
+
36
+ with_options if: unless_condition do
37
+ validates :stream_seq, presence: true, if: -> { self.class.has_column?(:stream_seq) }
38
+ # uniqueness scoped to stream when both present
39
+ if has_column?(:stream) && has_column?(:stream_seq)
38
40
  validates :stream_seq, uniqueness: { scope: :stream }
39
- else
41
+ elsif has_column?(:stream_seq)
40
42
  validates :stream_seq, uniqueness: true
41
43
  end
42
44
  end
43
45
 
44
- validates :subject, presence: true, if: -> { self.class.column?(:subject) }
45
-
46
- if self.class.column?(:status)
47
- STATUSES = %w[received processing processed failed].freeze
48
- validates :status, inclusion: { in: STATUSES }
49
- end
50
-
51
- scope :processed, -> { where(status: 'processed') }, if: -> { column?(:status) }
46
+ validates :subject, presence: true, if: -> { self.class.has_column?(:subject) }
52
47
 
53
48
  before_validation do
54
- self.status ||= 'received' if self.class.column?(:status) && status.blank?
55
- self.received_at ||= Time.now.utc if self.class.column?(:received_at) && received_at.blank?
49
+ self.status ||= 'received' if self.class.has_column?(:status) && status.blank?
50
+ if self.class.has_column?(:received_at) && received_at.blank?
51
+ self.received_at ||= Time.now.utc
52
+ end
56
53
  end
57
54
 
58
55
  def processed?
59
- if self.class.column?(:processed_at)
56
+ if self.class.has_column?(:processed_at)
60
57
  processed_at.present?
61
- elsif self.class.column?(:status)
58
+ elsif self.class.has_column?(:status)
62
59
  status == 'processed'
63
60
  else
64
61
  false
@@ -68,8 +65,12 @@ module JetstreamBridge
68
65
  def payload_hash
69
66
  v = self[:payload]
70
67
  case v
71
- when String then JSON.parse(v) rescue {}
72
- when Hash then v
68
+ when String then begin
69
+ JSON.parse(v)
70
+ rescue StandardError
71
+ {}
72
+ end
73
+ when Hash then v
73
74
  else v.respond_to?(:as_json) ? v.as_json : {}
74
75
  end
75
76
  end
@@ -81,15 +82,17 @@ module JetstreamBridge
81
82
  raise_missing_ar!('Inbox', method_name)
82
83
  end
83
84
 
84
- def respond_to_missing?(_name, _priv = false) = false
85
+ def respond_to_missing?(_m, _p = false)
86
+ false
87
+ end
85
88
 
86
89
  private
87
90
 
88
91
  def raise_missing_ar!(which, method_name)
89
92
  raise(
90
93
  "#{which} requires ActiveRecord (tried to call ##{method_name}). " \
91
- 'Enable `use_inbox` only in apps with ActiveRecord, or add ' \
92
- '`gem "activerecord"` to your Gemfile.'
94
+ 'Enable `use_inbox` only in apps with ActiveRecord, or add ' \
95
+ '`gem "activerecord"` to your Gemfile.'
93
96
  )
94
97
  end
95
98
  end
@@ -3,80 +3,68 @@
3
3
  begin
4
4
  require 'active_record'
5
5
  rescue LoadError
6
- # No-op; we provide a shim below if AR is missing.
6
+ # No-op; shim defined below.
7
7
  end
8
8
 
9
9
  module JetstreamBridge
10
- # Default Outbox model when `use_outbox` is enabled.
11
- # Works with event-centric columns and stays compatible with legacy resource_* fields.
12
10
  if defined?(ActiveRecord::Base)
13
11
  class OutboxEvent < ActiveRecord::Base
14
12
  self.table_name = 'jetstream_outbox_events'
15
13
 
16
14
  class << self
17
- def column?(name) = column_names.include?(name.to_s)
18
-
19
- def attribute_json?(name)
20
- return false unless respond_to?(:attribute_types) && attribute_types.key?(name.to_s)
21
- attribute_types[name.to_s].to_s.downcase.include?('json')
15
+ # Safe column presence check that never boots a connection during class load.
16
+ def has_column?(name)
17
+ return false unless ar_connected?
18
+ connection.schema_cache.columns_hash(table_name).key?(name.to_s)
19
+ rescue ActiveRecord::ConnectionNotEstablished, ActiveRecord::NoDatabaseError
20
+ false
22
21
  end
23
- end
24
22
 
25
- # JSON casting fallback if column is text
26
- if column?(:payload)
27
- serialize :payload, coder: JSON unless attribute_json?(:payload)
28
- end
29
- if column?(:headers)
30
- serialize :headers, coder: JSON unless attribute_json?(:headers)
23
+ def ar_connected?
24
+ # Use connected? and avoid retrieving/creating a connection
25
+ ActiveRecord::Base.connected? && connection_pool.active_connection?
26
+ rescue
27
+ false
28
+ end
31
29
  end
32
30
 
33
- # Validations (guarded by column existence)
34
- validates :payload, presence: true, if: -> { self.class.column?(:payload) }
31
+ # ---- Validations guarded by safe schema checks (evaluated at validation time) ----
32
+ validates :payload, presence: true, if: -> { self.class.has_column?(:payload) }
35
33
 
36
- if self.class.column?(:event_id)
34
+ with_options if: -> { self.class.has_column?(:event_id) } do
37
35
  validates :event_id, presence: true, uniqueness: true
38
- else
39
- validates :resource_type, presence: true, if: -> { self.class.column?(:resource_type) }
40
- validates :resource_id, presence: true, if: -> { self.class.column?(:resource_id) }
41
- validates :event_type, presence: true, if: -> { self.class.column?(:event_type) }
42
36
  end
43
37
 
44
- validates :subject, presence: true, if: -> { self.class.column?(:subject) }
45
-
46
- if self.class.column?(:status)
47
- STATUSES = %w[pending publishing sent failed].freeze
48
- validates :status, inclusion: { in: STATUSES }
38
+ with_options if: -> { !self.class.has_column?(:event_id) } do
39
+ validates :resource_type, presence: true, if: -> { self.class.has_column?(:resource_type) }
40
+ validates :resource_id, presence: true, if: -> { self.class.has_column?(:resource_id) }
41
+ validates :event_type, presence: true, if: -> { self.class.has_column?(:event_type) }
49
42
  end
50
43
 
51
- if self.class.column?(:attempts)
52
- validates :attempts, numericality: { only_integer: true, greater_than_or_equal_to: 0 }
53
- end
44
+ validates :subject, presence: true, if: -> { self.class.has_column?(:subject) }
54
45
 
55
- # Scopes (optional)
56
- scope :pending, -> { where(status: 'pending') }, if: -> { column?(:status) }
57
- scope :publishing, -> { where(status: 'publishing') }, if: -> { column?(:status) }
58
- scope :failed, -> { where(status: 'failed') }, if: -> { column?(:status) }
59
- scope :sent, -> { where(status: 'sent') }, if: -> { column?(:status) }
60
- scope :ready_to_send, -> { where(status: %w[pending failed]) }, if: -> { column?(:status) }
46
+ validates :attempts, numericality: { only_integer: true, greater_than_or_equal_to: 0 },
47
+ if: -> { self.class.has_column?(:attempts) }
61
48
 
49
+ # Defaults that do not require schema at load time
62
50
  before_validation do
63
51
  now = Time.now.utc
64
- self.status ||= 'pending' if self.class.column?(:status) && status.blank?
65
- self.enqueued_at ||= now if self.class.column?(:enqueued_at) && enqueued_at.blank?
66
- self.attempts = 0 if self.class.column?(:attempts) && attempts.nil?
52
+ self.status ||= 'pending' if self.class.has_column?(:status) && status.blank?
53
+ self.enqueued_at ||= now if self.class.has_column?(:enqueued_at) && enqueued_at.blank?
54
+ self.attempts = 0 if self.class.has_column?(:attempts) && attempts.nil?
67
55
  end
68
56
 
69
- # Helpers (no-ops if columns missing)
57
+ # Helpers
70
58
  def mark_sent!
71
59
  now = Time.now.utc
72
- self.status = 'sent' if self.class.column?(:status)
73
- self.sent_at = now if self.class.column?(:sent_at)
60
+ self.status = 'sent' if self.class.has_column?(:status)
61
+ self.sent_at = now if self.class.has_column?(:sent_at)
74
62
  save!
75
63
  end
76
64
 
77
65
  def mark_failed!(err_msg)
78
- self.status = 'failed' if self.class.column?(:status)
79
- self.last_error = err_msg if self.class.column?(:last_error)
66
+ self.status = 'failed' if self.class.has_column?(:status)
67
+ self.last_error = err_msg if self.class.has_column?(:last_error)
80
68
  save!
81
69
  end
82
70
 
@@ -90,17 +78,15 @@ module JetstreamBridge
90
78
  end
91
79
  end
92
80
  else
93
- # Shim: friendly error if AR is not available.
81
+ # Shim: loud failure if AR isn't present but someone calls the model.
94
82
  class OutboxEvent
95
83
  class << self
96
84
  def method_missing(method_name, *_args, &_block)
97
85
  raise_missing_ar!('Outbox', method_name)
98
86
  end
99
-
100
- def respond_to_missing?(_name, _priv = false) = false
87
+ def respond_to_missing?(_m, _p = false) = false
101
88
 
102
89
  private
103
-
104
90
  def raise_missing_ar!(which, method_name)
105
91
  raise(
106
92
  "#{which} requires ActiveRecord (tried to call ##{method_name}). " \
@@ -1,10 +1,35 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'rails/railtie'
4
-
3
+ # lib/jetstream_bridge/railtie.rb
5
4
  module JetstreamBridge
6
- # Railtie for JetstreamBridge.
7
5
  class Railtie < ::Rails::Railtie
6
+ initializer 'jetstream_bridge.defer_model_tweaks' do
7
+ ActiveSupport.on_load(:active_record) do
8
+ ActiveSupport::Reloader.to_prepare do
9
+ # Skip if not connected (e.g., non-DB rake tasks)
10
+ begin
11
+ next unless ActiveRecord::Base.connected?
12
+ rescue StandardError
13
+ next
14
+ end
15
+
16
+ [JetstreamBridge::OutboxEvent, JetstreamBridge::InboxEvent].each do |klass|
17
+ next unless klass.table_exists?
18
+
19
+ %w[payload headers].each do |attr|
20
+ next unless klass.columns_hash.key?(attr)
21
+
22
+ # Only add serialize if the column is not JSON/JSONB
23
+ type = klass.type_for_attribute(attr)
24
+ klass.serialize attr.to_sym, coder: JSON unless type&.json?
25
+ end
26
+ rescue ActiveRecord::StatementInvalid, ActiveRecord::ConnectionNotEstablished
27
+ # Ignore in tasks/environments without DB
28
+ end
29
+ end
30
+ end
31
+ end
32
+
8
33
  rake_tasks do
9
34
  load File.expand_path('tasks/install.rake', __dir__)
10
35
  end
@@ -4,5 +4,5 @@
4
4
  #
5
5
  # Version constant for the gem.
6
6
  module JetstreamBridge
7
- VERSION = '1.7.0'
7
+ VERSION = '1.8.0'
8
8
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jetstream_bridge
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.7.0
4
+ version: 1.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Attara