jetstream_bridge 1.8.0 → 1.10.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: 4b7171c9e1932f55d61a904c1e372b65ba56d2fa0eced6f72ac42b6d4f3f7020
4
- data.tar.gz: 8f6defde8bdf18e5421401a5b357c12b4d958efeaa91bf4963d4e253e29551ef
3
+ metadata.gz: 1fef54e5e8426e80c735e83ef11cf816643af9a0a8c3fac97089239f0b3743a7
4
+ data.tar.gz: 17442379f0f1a984fd89976b814a69752c83ec7efe4cc33602fb2998602999ce
5
5
  SHA512:
6
- metadata.gz: 798ccab4db891332fd08acfa888e400a06b49d79e1ce835729f59536e365fb953cb12ace04e3f55973823716c0b8b955245f0f7a6dd5c1895a35cd403eb8ee76
7
- data.tar.gz: 36c5b015c44a74213c11bd5bcd769dfd97409b4c022b9c3c569d587ff2c356a9b7eb59c36772be9601fe11f7a8a8fa4551f36a44a5ab4046f07f129c87b0c7b8
6
+ metadata.gz: 690ec24627cce26f73db546832083ed823ef4481451523260daf0d3802376ac999f631f8d56abd5a949d8a942210881b9da8656c0489264328215c5428db26b4
7
+ data.tar.gz: a7d17b77244fe5c3c260dad42c31cba8ddeef4988d96a0722cafc46cf5792d02e91f23332d0873d0230f624a331563652f2928e2219fde1572412dd6a4cca440
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.8.0)
4
+ jetstream_bridge (1.10.0)
5
5
  activerecord (>= 6.0)
6
6
  activesupport (>= 6.0)
7
7
  nats-pure (~> 2.4)
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JetstreamBridge
4
+ module ModelCodecSetup
5
+ module_function
6
+
7
+ def apply!
8
+ return unless ar_connected?
9
+
10
+ [JetstreamBridge::OutboxEvent, JetstreamBridge::InboxEvent].each { |k| apply_to(k) }
11
+ end
12
+
13
+ def apply_to(klass)
14
+ return unless table_exists_safely?(klass)
15
+
16
+ %w[payload headers].each do |attr|
17
+ next unless column?(klass, attr)
18
+ next if json_column?(klass, attr) || already_serialized?(klass, attr)
19
+
20
+ klass.serialize attr.to_sym, coder: JSON
21
+ end
22
+ rescue ActiveRecord::StatementInvalid, ActiveRecord::ConnectionNotEstablished, ActiveRecord::NoDatabaseError
23
+ # ignore when schema isn’t available yet
24
+ end
25
+
26
+ # --- helpers ---
27
+
28
+ def ar_connected?
29
+ ActiveRecord::Base.connected?
30
+ rescue StandardError
31
+ false
32
+ end
33
+
34
+ def table_exists_safely?(klass)
35
+ klass.table_exists?
36
+ rescue StandardError
37
+ false
38
+ end
39
+
40
+ def column?(klass, attr)
41
+ klass.columns_hash.key?(attr)
42
+ rescue StandardError
43
+ false
44
+ end
45
+
46
+ def json_column?(klass, attr)
47
+ sql = klass.columns_hash.fetch(attr).sql_type.to_s.downcase
48
+ sql.include?('json') # covers json & jsonb
49
+ rescue StandardError
50
+ false
51
+ end
52
+
53
+ def already_serialized?(klass, attr)
54
+ klass.attribute_types.fetch(attr, nil).is_a?(ActiveRecord::Type::Serialized)
55
+ rescue StandardError
56
+ false
57
+ end
58
+ end
59
+ end
@@ -12,9 +12,9 @@ module JetstreamBridge
12
12
  self.table_name = 'jetstream_inbox_events'
13
13
 
14
14
  class << self
15
+ # Safe column presence check that never boots a connection during class load.
15
16
  def has_column?(name)
16
17
  return false unless ar_connected?
17
-
18
18
  connection.schema_cache.columns_hash(table_name).key?(name.to_s)
19
19
  rescue ActiveRecord::ConnectionNotEstablished, ActiveRecord::NoDatabaseError
20
20
  false
@@ -22,36 +22,51 @@ module JetstreamBridge
22
22
 
23
23
  def ar_connected?
24
24
  ActiveRecord::Base.connected? && connection_pool.active_connection?
25
- rescue StandardError
25
+ rescue
26
26
  false
27
27
  end
28
28
  end
29
29
 
30
- # Validations guarded by safe checks
31
- if_condition = -> { self.class.has_column?(:event_id) }
32
- unless_condition = -> { !self.class.has_column?(:event_id) }
30
+ # ---- Validations (NO with_options; guard everything with procs) ----
33
31
 
34
- validates :event_id, presence: true, uniqueness: true, if: if_condition
32
+ # Preferred dedupe key
33
+ validates :event_id,
34
+ presence: true,
35
+ uniqueness: true,
36
+ if: -> { self.class.has_column?(:event_id) }
35
37
 
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)
40
- validates :stream_seq, uniqueness: { scope: :stream }
41
- elsif has_column?(:stream_seq)
42
- validates :stream_seq, uniqueness: true
43
- end
44
- end
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
- validates :subject, presence: true, if: -> { self.class.has_column?(:subject) }
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
+ }
47
50
 
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
+ }
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 ----
48
64
  before_validation do
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
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?
53
67
  end
54
68
 
69
+ # ---- Helpers ----
55
70
  def processed?
56
71
  if self.class.has_column?(:processed_at)
57
72
  processed_at.present?
@@ -65,34 +80,27 @@ module JetstreamBridge
65
80
  def payload_hash
66
81
  v = self[:payload]
67
82
  case v
68
- when String then begin
69
- JSON.parse(v)
70
- rescue StandardError
71
- {}
72
- end
73
- when Hash then v
83
+ when String then JSON.parse(v) rescue {}
84
+ when Hash then v
74
85
  else v.respond_to?(:as_json) ? v.as_json : {}
75
86
  end
76
87
  end
77
88
  end
78
89
  else
90
+ # Shim: loud failure if AR isn't present but someone calls the model.
79
91
  class InboxEvent
80
92
  class << self
81
93
  def method_missing(method_name, *_args, &_block)
82
94
  raise_missing_ar!('Inbox', method_name)
83
95
  end
84
-
85
- def respond_to_missing?(_m, _p = false)
86
- false
87
- end
96
+ def respond_to_missing?(_m, _p = false) = false
88
97
 
89
98
  private
90
-
91
99
  def raise_missing_ar!(which, method_name)
92
100
  raise(
93
101
  "#{which} requires ActiveRecord (tried to call ##{method_name}). " \
94
- 'Enable `use_inbox` only in apps with ActiveRecord, or add ' \
95
- '`gem "activerecord"` to your Gemfile.'
102
+ 'Enable `use_inbox` only in apps with ActiveRecord, or add ' \
103
+ '`gem \"activerecord\"` to your Gemfile.'
96
104
  )
97
105
  end
98
106
  end
@@ -21,40 +21,54 @@ module JetstreamBridge
21
21
  end
22
22
 
23
23
  def ar_connected?
24
- # Use connected? and avoid retrieving/creating a connection
24
+ # Avoid creating a connection; rescue if pool isn't set yet.
25
25
  ActiveRecord::Base.connected? && connection_pool.active_connection?
26
26
  rescue
27
27
  false
28
28
  end
29
29
  end
30
30
 
31
- # ---- Validations guarded by safe schema checks (evaluated at validation time) ----
32
- validates :payload, presence: true, if: -> { self.class.has_column?(:payload) }
31
+ # ---- Validations guarded by safe schema checks (no with_options) ----
32
+ validates :payload,
33
+ presence: true,
34
+ if: -> { self.class.has_column?(:payload) }
33
35
 
34
- with_options if: -> { self.class.has_column?(:event_id) } do
35
- validates :event_id, presence: true, uniqueness: true
36
- end
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) }
37
41
 
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) }
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) }
46
+
47
+ validates :resource_id,
48
+ presence: true,
49
+ if: -> { !self.class.has_column?(:event_id) && self.class.has_column?(:resource_id) }
43
50
 
44
- validates :subject, presence: true, if: -> { self.class.has_column?(:subject) }
51
+ validates :event_type,
52
+ presence: true,
53
+ if: -> { !self.class.has_column?(:event_id) && self.class.has_column?(:event_type) }
45
54
 
46
- validates :attempts, numericality: { only_integer: true, greater_than_or_equal_to: 0 },
55
+ validates :subject,
56
+ presence: true,
57
+ if: -> { self.class.has_column?(:subject) }
58
+
59
+ validates :attempts,
60
+ numericality: { only_integer: true, greater_than_or_equal_to: 0 },
47
61
  if: -> { self.class.has_column?(:attempts) }
48
62
 
49
- # Defaults that do not require schema at load time
63
+ # ---- Defaults that do not require schema at load time ----
50
64
  before_validation do
51
65
  now = Time.now.utc
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?
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?
55
69
  end
56
70
 
57
- # Helpers
71
+ # ---- Helpers ----
58
72
  def mark_sent!
59
73
  now = Time.now.utc
60
74
  self.status = 'sent' if self.class.has_column?(:status)
@@ -84,9 +98,11 @@ module JetstreamBridge
84
98
  def method_missing(method_name, *_args, &_block)
85
99
  raise_missing_ar!('Outbox', method_name)
86
100
  end
101
+
87
102
  def respond_to_missing?(_m, _p = false) = false
88
103
 
89
104
  private
105
+
90
106
  def raise_missing_ar!(which, method_name)
91
107
  raise(
92
108
  "#{which} requires ActiveRecord (tried to call ##{method_name}). " \
@@ -1,32 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # lib/jetstream_bridge/railtie.rb
3
+ require_relative 'core/model_codec_setup'
4
+
4
5
  module JetstreamBridge
5
6
  class Railtie < ::Rails::Railtie
6
7
  initializer 'jetstream_bridge.defer_model_tweaks' do
7
8
  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
9
+ ActiveSupport::Reloader.to_prepare { JetstreamBridge::ModelCodecSetup.apply! }
30
10
  end
31
11
  end
32
12
 
@@ -4,5 +4,5 @@
4
4
  #
5
5
  # Version constant for the gem.
6
6
  module JetstreamBridge
7
- VERSION = '1.8.0'
7
+ VERSION = '1.10.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.8.0
4
+ version: 1.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Attara
@@ -192,6 +192,7 @@ files:
192
192
  - lib/jetstream_bridge/core/connection.rb
193
193
  - lib/jetstream_bridge/core/duration.rb
194
194
  - lib/jetstream_bridge/core/logging.rb
195
+ - lib/jetstream_bridge/core/model_codec_setup.rb
195
196
  - lib/jetstream_bridge/core/model_utils.rb
196
197
  - lib/jetstream_bridge/models/inbox_event.rb
197
198
  - lib/jetstream_bridge/models/outbox_event.rb