jetstream_bridge 1.7.0 → 1.9.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: 601869d9b83276fd0e3b6a249dafc1bb7a749ffe6aade556077b60b95eb24124
4
+ data.tar.gz: b7ea75cdabb21f5f71d5ff447cd4a449c470a56ff6a3723737895c1cfa260bbc
5
5
  SHA512:
6
- metadata.gz: 342b6b3431d3768b843060402ededa4dcca39100c1d0c005153a3ea4f758a9ab6aff9a09e5d608444acbb332766cfb926f3c1555afb8d9df882cfcdb7a8b405f
7
- data.tar.gz: eb257f387a461444121cb304c36e9fa3ce13062c81a8bc43c0cda4e6f6c706c2efa8cc7a4cf3fdfc721cedeab1d11f0ca8bb00e943644c01575d051f5e143c19
6
+ metadata.gz: 4de9e146864e60fda36d68380e7bf5485d317d599c689d2594e719c1f71f73da3667d43add5e646289a2e5dc8f4dc93495b2f615b99d90c120d4674684b415fe
7
+ data.tar.gz: 4b4f148a4dea6c38c108f84f07bb179e601faf8aec06f01cbf495a6b645e14dc11ce805d69ab7c91dbd747966f6f44866d19a9b429e347617fa06e208b159c8c
data/.rubocop.yml CHANGED
@@ -5,10 +5,6 @@ plugins:
5
5
  - rubocop-performance
6
6
  - rubocop-packaging
7
7
 
8
- # If you keep a generated TODO, uncomment:
9
- # inherit_from:
10
- # - .rubocop_todo.yml
11
-
12
8
  AllCops:
13
9
  NewCops: enable
14
10
  TargetRubyVersion: 2.7 # match gemspec minimum to avoid false positives
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.9.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,74 @@
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
+ # 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
21
+ end
18
22
 
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')
23
+ def ar_connected?
24
+ ActiveRecord::Base.connected? && connection_pool.active_connection?
25
+ rescue
26
+ false
22
27
  end
23
28
  end
24
29
 
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)
30
- end
30
+ # ---- Validations (NO with_options; guard everything with procs) ----
31
31
 
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)
38
- validates :stream_seq, uniqueness: { scope: :stream }
39
- else
40
- validates :stream_seq, uniqueness: true
41
- end
42
- end
32
+ # Preferred dedupe key
33
+ validates :event_id,
34
+ presence: true,
35
+ uniqueness: true,
36
+ if: -> { self.class.has_column?(:event_id) }
43
37
 
44
- validates :subject, presence: true, if: -> { self.class.column?(:subject) }
38
+ # Fallback to (stream, stream_seq) when event_id column not present
39
+ validates :stream_seq,
40
+ presence: true,
41
+ if: -> { !self.class.has_column?(:event_id) && self.class.has_column?(:stream_seq) }
45
42
 
46
- if self.class.column?(:status)
47
- STATUSES = %w[received processing processed failed].freeze
48
- validates :status, inclusion: { in: STATUSES }
49
- end
43
+ validates :stream_seq,
44
+ uniqueness: { scope: :stream },
45
+ if: -> {
46
+ !self.class.has_column?(:event_id) &&
47
+ self.class.has_column?(:stream_seq) &&
48
+ self.class.has_column?(:stream)
49
+ }
50
50
 
51
- scope :processed, -> { where(status: 'processed') }, if: -> { column?(:status) }
51
+ validates :stream_seq,
52
+ uniqueness: true,
53
+ if: -> {
54
+ !self.class.has_column?(:event_id) &&
55
+ self.class.has_column?(:stream_seq) &&
56
+ !self.class.has_column?(:stream)
57
+ }
52
58
 
59
+ validates :subject,
60
+ presence: true,
61
+ if: -> { self.class.has_column?(:subject) }
62
+
63
+ # ---- Defaults that do not require schema at load time ----
53
64
  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?
65
+ self.status ||= 'received' if self.class.has_column?(:status) && status.blank?
66
+ self.received_at ||= Time.now.utc if self.class.has_column?(:received_at) && received_at.blank?
56
67
  end
57
68
 
69
+ # ---- Helpers ----
58
70
  def processed?
59
- if self.class.column?(:processed_at)
71
+ if self.class.has_column?(:processed_at)
60
72
  processed_at.present?
61
- elsif self.class.column?(:status)
73
+ elsif self.class.has_column?(:status)
62
74
  status == 'processed'
63
75
  else
64
76
  false
@@ -75,21 +87,20 @@ module JetstreamBridge
75
87
  end
76
88
  end
77
89
  else
90
+ # Shim: loud failure if AR isn't present but someone calls the model.
78
91
  class InboxEvent
79
92
  class << self
80
93
  def method_missing(method_name, *_args, &_block)
81
94
  raise_missing_ar!('Inbox', method_name)
82
95
  end
83
-
84
- def respond_to_missing?(_name, _priv = false) = false
96
+ def respond_to_missing?(_m, _p = false) = false
85
97
 
86
98
  private
87
-
88
99
  def raise_missing_ar!(which, method_name)
89
100
  raise(
90
101
  "#{which} requires ActiveRecord (tried to call ##{method_name}). " \
91
102
  'Enable `use_inbox` only in apps with ActiveRecord, or add ' \
92
- '`gem "activerecord"` to your Gemfile.'
103
+ '`gem \"activerecord\"` to your Gemfile.'
93
104
  )
94
105
  end
95
106
  end
@@ -3,80 +3,82 @@
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)
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
21
+ end
18
22
 
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')
23
+ def ar_connected?
24
+ # Avoid creating a connection; rescue if pool isn't set yet.
25
+ ActiveRecord::Base.connected? && connection_pool.active_connection?
26
+ rescue
27
+ false
22
28
  end
23
29
  end
24
30
 
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)
31
- end
31
+ # ---- Validations guarded by safe schema checks (no with_options) ----
32
+ validates :payload,
33
+ presence: true,
34
+ if: -> { self.class.has_column?(:payload) }
32
35
 
33
- # Validations (guarded by column existence)
34
- validates :payload, presence: true, if: -> { self.class.column?(:payload) }
36
+ # Preferred path when event_id exists
37
+ validates :event_id,
38
+ presence: true,
39
+ uniqueness: true,
40
+ if: -> { self.class.has_column?(:event_id) }
35
41
 
36
- if self.class.column?(:event_id)
37
- 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
- end
42
+ # Fallback legacy fields when event_id is absent
43
+ validates :resource_type,
44
+ presence: true,
45
+ if: -> { !self.class.has_column?(:event_id) && self.class.has_column?(:resource_type) }
43
46
 
44
- validates :subject, presence: true, if: -> { self.class.column?(:subject) }
47
+ validates :resource_id,
48
+ presence: true,
49
+ if: -> { !self.class.has_column?(:event_id) && self.class.has_column?(:resource_id) }
45
50
 
46
- if self.class.column?(:status)
47
- STATUSES = %w[pending publishing sent failed].freeze
48
- validates :status, inclusion: { in: STATUSES }
49
- end
51
+ validates :event_type,
52
+ presence: true,
53
+ if: -> { !self.class.has_column?(:event_id) && self.class.has_column?(:event_type) }
50
54
 
51
- if self.class.column?(:attempts)
52
- validates :attempts, numericality: { only_integer: true, greater_than_or_equal_to: 0 }
53
- end
55
+ validates :subject,
56
+ presence: true,
57
+ if: -> { self.class.has_column?(:subject) }
54
58
 
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) }
59
+ validates :attempts,
60
+ numericality: { only_integer: true, greater_than_or_equal_to: 0 },
61
+ if: -> { self.class.has_column?(:attempts) }
61
62
 
63
+ # ---- Defaults that do not require schema at load time ----
62
64
  before_validation do
63
65
  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?
66
+ self.status ||= 'pending' if self.class.has_column?(:status) && status.blank?
67
+ self.enqueued_at ||= now if self.class.has_column?(:enqueued_at) && enqueued_at.blank?
68
+ self.attempts = 0 if self.class.has_column?(:attempts) && attempts.nil?
67
69
  end
68
70
 
69
- # Helpers (no-ops if columns missing)
71
+ # ---- Helpers ----
70
72
  def mark_sent!
71
73
  now = Time.now.utc
72
- self.status = 'sent' if self.class.column?(:status)
73
- self.sent_at = now if self.class.column?(:sent_at)
74
+ self.status = 'sent' if self.class.has_column?(:status)
75
+ self.sent_at = now if self.class.has_column?(:sent_at)
74
76
  save!
75
77
  end
76
78
 
77
79
  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)
80
+ self.status = 'failed' if self.class.has_column?(:status)
81
+ self.last_error = err_msg if self.class.has_column?(:last_error)
80
82
  save!
81
83
  end
82
84
 
@@ -90,14 +92,14 @@ module JetstreamBridge
90
92
  end
91
93
  end
92
94
  else
93
- # Shim: friendly error if AR is not available.
95
+ # Shim: loud failure if AR isn't present but someone calls the model.
94
96
  class OutboxEvent
95
97
  class << self
96
98
  def method_missing(method_name, *_args, &_block)
97
99
  raise_missing_ar!('Outbox', method_name)
98
100
  end
99
101
 
100
- def respond_to_missing?(_name, _priv = false) = false
102
+ def respond_to_missing?(_m, _p = false) = false
101
103
 
102
104
  private
103
105
 
@@ -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.9.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.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Attara