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 +4 -4
- data/Gemfile.lock +1 -3
- data/lib/jetstream_bridge/models/inbox_event.rb +40 -37
- data/lib/jetstream_bridge/models/outbox_event.rb +34 -48
- data/lib/jetstream_bridge/railtie.rb +28 -3
- data/lib/jetstream_bridge/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4b7171c9e1932f55d61a904c1e372b65ba56d2fa0eced6f72ac42b6d4f3f7020
|
4
|
+
data.tar.gz: 8f6defde8bdf18e5421401a5b357c12b4d958efeaa91bf4963d4e253e29551ef
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
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
|
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
|
15
|
+
def has_column?(name)
|
16
|
+
return false unless ar_connected?
|
18
17
|
|
19
|
-
|
20
|
-
|
21
|
-
|
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
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
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
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
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
|
-
|
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.
|
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
|
55
|
-
|
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.
|
56
|
+
if self.class.has_column?(:processed_at)
|
60
57
|
processed_at.present?
|
61
|
-
elsif self.class.
|
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
|
72
|
-
|
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?(
|
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
|
-
|
92
|
-
|
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;
|
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
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
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
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
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
|
34
|
-
validates :payload, presence: true, if: -> { self.class.
|
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.
|
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
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
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.
|
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
|
-
|
56
|
-
|
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.
|
65
|
-
self.enqueued_at ||= now if self.class.
|
66
|
-
self.attempts = 0 if self.class.
|
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
|
57
|
+
# Helpers
|
70
58
|
def mark_sent!
|
71
59
|
now = Time.now.utc
|
72
|
-
self.status = 'sent' if self.class.
|
73
|
-
self.sent_at = now if self.class.
|
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.
|
79
|
-
self.last_error = err_msg if self.class.
|
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:
|
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
|
-
|
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
|