salesforce_streamer 1.2.2 → 2.1.1

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: 88a8ec9604b29ed48acfcf8ac1435fcacf199a7813eba8db8937038874132128
4
- data.tar.gz: 5b7c06be3a4467d41845e76578c95a9bb197a10017ee893eb2acc516b26ffbb0
3
+ metadata.gz: 4a92bbc0eaffa8152b8cdcc6b8eb35248a8fe1e996bc73410dc1ed4043acde08
4
+ data.tar.gz: '0787479213ab7bc07c07a6cc5d8c9034f5593aee613a28529e25dd51e8c3d9c2'
5
5
  SHA512:
6
- metadata.gz: 36d5ba79e4af94533266bc9ac52b5110b6a4f6305db9c60a6498b7218d7a5e8c7f48b9db008b582c56877845bb9a498618141da4af0c3e1b77a8173a0640733d
7
- data.tar.gz: d06777105574d0ff430888059ab1d1f61164f520da6b3b5a1c6408e983de3fe22d27fabf575e46a2d7f7ce14936e7af8423b8c7b433a296183a73da8716cd0e9
6
+ metadata.gz: 95e9b7ba4133fe7c7840e413677cff3121b6d4c94c9f1037b4fd91fe1a111dd6a1ac350075b987d68cd1fbcb72b733fbed3bd087353b6a075789a8fd15c014cb
7
+ data.tar.gz: 573892b3222c3240fb8d171b32f03f446bae3a0ff2184aa7bbbdf71f940ecc961cd84a634dda824a9ecb42acb13446e44b8d0d8f4f4dacb4cae0f1c555658cf0
@@ -9,10 +9,3 @@ update_configs:
9
9
  allowed_updates:
10
10
  - match:
11
11
  update_type: "all"
12
- automerged_updates:
13
- - match:
14
- dependency_type: "development"
15
- update_type: "all"
16
- - match:
17
- dependency_type: "production"
18
- update_type: "all"
@@ -0,0 +1,99 @@
1
+ # Changelog
2
+
3
+ Sorted so that the most recent change logs are near the top. Only significant
4
+ changes are logged in the change log.
5
+
6
+ ## 2020-08-17 Scott Serok [scott@renofi.com](mailto:scott@renofi.com)
7
+
8
+ v2.1 changes the expected interface of `Configuration#replay_adapter`.
9
+
10
+ Normally this breaking change would require a major version bump, but since the
11
+ functionality today is quiet broken we can call this a major bug fix.
12
+
13
+ The `config.replay_adapter` should be an object that has an interface like Hash.
14
+ It must respond to `[]` and `[]=`. By default the adapter is an empty hash. If
15
+ you want your push topic replayId to persist between restarts, then you should
16
+ implement a class with an appropriate interface.
17
+
18
+ ```ruby
19
+ class MyReplayAdapter
20
+ def [](channel)
21
+ MyPersistence.get(channel)
22
+ end
23
+
24
+ def []=(channel, replay_id)
25
+ MyPersistence.set(channel, replay_id)
26
+ end
27
+ end
28
+ ```
29
+
30
+ This change was sparked by a misunderstanding of the
31
+ `Restforce::Concerns::Streaming::ReplayExtension` replay handlers.
32
+ SalesforceStreamer can eliminate some complexity and fix a bug by delegating the
33
+ responsibility of maintaining the current replayId to that ReplayExtension. The
34
+ object will be used on each request/response cycle to record and read the latest
35
+ replayId as long as the object assigned to `config.replay_adapter` responds to
36
+ `[]` and `[]=`.
37
+
38
+ ## 2020-08-04 Scott Serok [scott@renofi.com](mailto:scott@renofi.com)
39
+
40
+ v2.0 is released as a major simplification of this library. There are 2
41
+ significant differences from a user's perspective.
42
+
43
+ 1. The YAML configuration requires minor edits to be compatible in v2.0.
44
+ 2. The built-in Redis persistance of the replayId field has been removed. You
45
+ should add a custom middleware and configure the new replay_adapter option.
46
+
47
+ ### PushTopic configuration changes
48
+
49
+ After upgrading to v2, the YAML configuration should be modified. Shift the
50
+ nested "salesforce" block to the left and remove the "salesforce" key.
51
+
52
+ Before v2:
53
+
54
+ name: "TopicName"
55
+ handler: "MyConstant"
56
+ salesforce:
57
+ query: "SELECT Id FROM Lead"
58
+
59
+ As of v2:
60
+
61
+ name: "TopicName"
62
+ handler: "MyConstant"
63
+ query: "SELECT Id FROM Lead"
64
+
65
+ ### Redis Persistance removed
66
+
67
+ The original intention of this library is to manage PushTopic definitions
68
+ and run an event machine that subscribes to the Salesforce Streaming API based
69
+ on those PushTopics.
70
+
71
+ The addition of managing the Replay ID adds unecessary complexity that can be
72
+ incorporated through customization, and so it's been removed. You might use a
73
+ recent commit of v1 of this library for reference how to implement Redis as the
74
+ persistence layer.
75
+
76
+ To record the replayId on every message we can add a piece of middleware
77
+
78
+ class RecordReplayIdMiddleware
79
+ def initialize(handler)
80
+ @handler = handler
81
+ end
82
+
83
+ def call(message)
84
+ @handler.call(message)
85
+ replay_id = message['event']['replayId']
86
+ topic_name = message['topic']
87
+ MyStore.record_replay_id(replay_id, topic_name)
88
+ end
89
+ end
90
+ SalesforceStreamer.config.use_middleware RecordReplayIdMiddleware
91
+
92
+ To retrieve the replayId before subscribing to a PushTopic,
93
+ configure an adapter that returns an integer.
94
+
95
+ SalesforceStreamer.config.replay_adapter = proc { |topic|
96
+ (MyStore.fetch_replay_id(topic.name) || -1).to_i
97
+ }
98
+
99
+ This will be used to set the replayId value when subscribing to the PushTopic.
@@ -1,9 +1,10 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- salesforce_streamer (1.2.2)
5
- faye (= 0.8.9)
6
- restforce (~> 4.2)
4
+ salesforce_streamer (2.1.1)
5
+ dry-initializer (~> 3.0)
6
+ faye (~> 1.4)
7
+ restforce (>= 4.2, < 6.0)
7
8
 
8
9
  GEM
9
10
  remote: https://rubygems.org/
@@ -12,15 +13,14 @@ GEM
12
13
  public_suffix (>= 2.0.2, < 5.0)
13
14
  ast (2.4.1)
14
15
  byebug (11.1.3)
15
- codecov (0.2.0)
16
- colorize
16
+ codecov (0.2.9)
17
17
  json
18
18
  simplecov
19
- colorize (0.8.1)
20
19
  cookiejar (0.3.3)
21
20
  diff-lcs (1.4.4)
22
21
  docile (1.3.2)
23
- em-http-request (1.1.6)
22
+ dry-initializer (3.0.3)
23
+ em-http-request (1.1.7)
24
24
  addressable (>= 2.3.4)
25
25
  cookiejar (!= 0.3.1)
26
26
  em-socksify (>= 0.3)
@@ -29,24 +29,26 @@ GEM
29
29
  em-socksify (0.3.2)
30
30
  eventmachine (>= 1.0.0.beta.4)
31
31
  eventmachine (1.2.7)
32
- faraday (1.0.0)
32
+ faraday (1.0.1)
33
33
  multipart-post (>= 1.2, < 3)
34
34
  faraday_middleware (1.0.0)
35
35
  faraday (~> 1.0)
36
- faye (0.8.9)
36
+ faye (1.4.0)
37
37
  cookiejar (>= 0.3.0)
38
- em-http-request (>= 0.3.0)
38
+ em-http-request (>= 1.1.6)
39
39
  eventmachine (>= 0.12.0)
40
- faye-websocket (>= 0.4.0)
40
+ faye-websocket (>= 0.11.0)
41
+ multi_json (>= 1.0.0)
41
42
  rack (>= 1.0.0)
42
- yajl-ruby (>= 1.0.0)
43
+ websocket-driver (>= 0.5.1)
43
44
  faye-websocket (0.11.0)
44
45
  eventmachine (>= 0.12.0)
45
46
  websocket-driver (>= 0.5.1)
46
47
  hashie (4.1.0)
47
48
  http_parser.rb (0.6.0)
48
49
  json (2.3.1)
49
- jwt (2.2.1)
50
+ jwt (2.2.2)
51
+ multi_json (1.15.0)
50
52
  multipart-post (2.1.1)
51
53
  parallel (1.19.2)
52
54
  parser (2.7.1.4)
@@ -56,11 +58,10 @@ GEM
56
58
  rainbow (3.0.0)
57
59
  rake (13.0.1)
58
60
  regexp_parser (1.7.1)
59
- restforce (4.2.2)
60
- faraday (>= 0.9.0, <= 1.0)
61
- faraday_middleware (>= 0.8.8, <= 1.0)
61
+ restforce (5.0.1)
62
+ faraday (>= 0.9.0, <= 2.0)
63
+ faraday_middleware (>= 0.8.8, <= 2.0)
62
64
  hashie (>= 1.2.0, < 5.0)
63
- json (>= 1.7.5)
64
65
  jwt (>= 1.5.6)
65
66
  rexml (3.2.4)
66
67
  rspec (3.9.0)
@@ -76,23 +77,23 @@ GEM
76
77
  diff-lcs (>= 1.2.0, < 2.0)
77
78
  rspec-support (~> 3.9.0)
78
79
  rspec-support (3.9.3)
79
- rubocop (0.88.0)
80
+ rubocop (0.90.0)
80
81
  parallel (~> 1.10)
81
82
  parser (>= 2.7.1.1)
82
83
  rainbow (>= 2.2.2, < 4.0)
83
84
  regexp_parser (>= 1.7)
84
85
  rexml
85
- rubocop-ast (>= 0.1.0, < 1.0)
86
+ rubocop-ast (>= 0.3.0, < 1.0)
86
87
  ruby-progressbar (~> 1.7)
87
88
  unicode-display_width (>= 1.4.0, < 2.0)
88
- rubocop-ast (0.1.0)
89
- parser (>= 2.7.0.1)
90
- rubocop-performance (1.7.0)
89
+ rubocop-ast (0.3.0)
90
+ parser (>= 2.7.1.4)
91
+ rubocop-performance (1.7.1)
91
92
  rubocop (>= 0.82.0)
92
- rubocop-rspec (1.42.0)
93
- rubocop (>= 0.87.0)
93
+ rubocop-rspec (1.43.2)
94
+ rubocop (~> 0.87)
94
95
  ruby-progressbar (1.10.1)
95
- simplecov (0.18.5)
96
+ simplecov (0.19.0)
96
97
  docile (~> 1.1)
97
98
  simplecov-html (~> 0.11)
98
99
  simplecov-html (0.12.2)
@@ -100,7 +101,6 @@ GEM
100
101
  websocket-driver (0.7.3)
101
102
  websocket-extensions (>= 0.1.0)
102
103
  websocket-extensions (0.1.5)
103
- yajl-ruby (1.4.1)
104
104
 
105
105
  PLATFORMS
106
106
  ruby
data/README.md CHANGED
@@ -20,33 +20,11 @@ And then execute:
20
20
 
21
21
  ## Usage
22
22
 
23
- ### Middleware
24
-
25
- You can initialize the streamer server with any number of middleware classes.
26
- When a message is received by a PushTopic subscription, the chain of middleware
27
- classes are executed before the message handler is called.
28
-
29
- ```ruby
30
- # config/initializers/streamer.rb
31
- class MySimpleMiddleware
32
- def initialize(handler)
33
- @handler = handler
34
- end
35
- def call(message)
36
- @handler.call(message)
37
- end
38
- end
39
-
40
- SalesforceStreamer.config.use_middleware MySimpleMiddleware
41
- ```
42
-
43
23
  ### Configure Push Topics
44
24
 
45
- Create a YAML file to configure your server subscriptions. The configuration
46
- for each subscription must have a nested `salesforce:` key. These settings will
47
- be synced to your Salesforce instance when the `-x` flag is set on the command
48
- line. For more information about the `replay:` and `notify_fields_for` options
49
- please see the Salesforce Streaming API reference documentation.
25
+ Create a YAML file to configure your PushTopic subscriptions. When streamer
26
+ starts up it will check for any differences between Salesforce PushTopics and
27
+ this yaml and update any differences when `config.manage_topics = true`.
50
28
 
51
29
  ```yaml
52
30
  # config/streamer.yml
@@ -55,12 +33,11 @@ base: &DEFAULT
55
33
  accounts:
56
34
  handler: "AccountChangeHandler"
57
35
  replay: -1
58
- salesforce:
59
- name: "AllAccounts"
60
- api_version: "41.0"
61
- description: "Sync Accounts"
62
- notify_for_fields: "Referenced"
63
- query: "Select Id, Name From Account"
36
+ name: "AllAccounts"
37
+ api_version: "49.0"
38
+ description: "Sync Accounts"
39
+ notify_for_fields: "Referenced"
40
+ query: "Select Id, Name From Account"
64
41
 
65
42
  development:
66
43
  <<: *DEFAULT
@@ -72,14 +49,6 @@ production:
72
49
  <<: *DEFAULT
73
50
  ```
74
51
 
75
- It's important to note that the way push topics are managed is by the Salesforce
76
- name attribute. This should uniquely identify each push topic. It is not
77
- recommended to change the name of your push topic definitions; otherwise, the
78
- push topic manager will not find a push topic in Salesforce resulting in the
79
- creation of a brand new push topic. If the push topic manager identifies a
80
- difference in any of the other Salesforce attributes, then it will update the
81
- push topic in Salesforce before starting the streaming server.
82
-
83
52
  ### Define Message Handlers
84
53
 
85
54
  Define your handlers somewhere in your project. They must respond to either
@@ -89,14 +58,17 @@ Define your handlers somewhere in your project. They must respond to either
89
58
  # lib/account_change_handler.rb
90
59
  # Handle account changes inline
91
60
  class AccountChangeHandler
92
- def self.call(message)
93
- puts message
61
+ class << self
62
+ def call(message)
63
+ puts message
64
+ end
94
65
  end
95
66
  end
96
67
 
97
68
  # Handle account changes asynchronously
98
69
  class AccountChangeHandler
99
70
  include Sidekiq::Worker
71
+
100
72
  def perform(message)
101
73
  puts message
102
74
  end
@@ -114,13 +86,13 @@ Configure the `SalesforceStreamer` module.
114
86
  ```ruby
115
87
  # config/initializers/salesforce_streamer.rb
116
88
 
117
- require 'redis'
118
- require 'connection_pool'
119
-
120
- SalesforceStreamer.config.redis_connection = ConnectionPool.new(size: 5, timeout: 5) { Redis.new }
121
- SalesforceStreamer.config.logger = Logger.new(STDERR, level: 'INFO')
122
- SalesforceStreamer.config.exception_adapter = proc { |e| puts e }
123
- SalesforceStreamer.config.manage_topics = true
89
+ SalesforceStreamer.configure do |config|
90
+ config.logger = Logger.new(STDERR, level: 'INFO')
91
+ config.exception_adapter = proc { |e| puts e }
92
+ config.replay_adapter = MyReplayAdapter
93
+ config.use_middleware AfterMessageReceived
94
+ config.manage_topics = true
95
+ end
124
96
  ```
125
97
 
126
98
  ### Launch The Streamer
@@ -159,6 +131,48 @@ By default, the executable will load the YAML based on the `RACK_ENV` environmen
159
131
  variable, or default to `:development` if not set. You can override this by
160
132
  setting the `config.environment = :integration`
161
133
 
134
+ ### Message Handling Middleware
135
+
136
+ You can initialize the streamer server with any number of middleware classes.
137
+ When a message is received by a PushTopic subscription, the chain of middleware
138
+ classes are executed before the message handler is called.
139
+
140
+ ```ruby
141
+ # config/initializers/streamer.rb
142
+ class MySimpleMiddleware
143
+ def initialize(handler)
144
+ @handler = handler
145
+ end
146
+
147
+ def call(message)
148
+ @handler.call(message)
149
+ end
150
+ end
151
+
152
+ SalesforceStreamer.config.use_middleware MySimpleMiddleware
153
+ ```
154
+
155
+ ### ReplayAdapter
156
+
157
+ The `config.replay_adapter` should be an object that has an interface like Hash.
158
+ It must respond to `[]` and `[]=`. By default the adapter is an empty hash. If
159
+ you want your push topic replayId to persist between restarts, then you should
160
+ implement a class with an appropriate interface.
161
+
162
+ ```ruby
163
+ class MyReplayAdapter
164
+ def [](channel)
165
+ Persistence.get(channel)
166
+ end
167
+
168
+ def []=(channel, replay_id)
169
+ Persistence.set(channel, replay_id)
170
+ end
171
+ end
172
+ ```
173
+
174
+ This adapter will be used directly by `Restforce::ReplayExtension`.
175
+
162
176
  ## Development
163
177
 
164
178
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake rspec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
data/Rakefile CHANGED
@@ -5,4 +5,4 @@ require 'rubocop/rake_task'
5
5
  RSpec::Core::RakeTask.new(:spec)
6
6
  RuboCop::RakeTask.new
7
7
 
8
- task default: %i[rubocop:auto_correct spec]
8
+ task default: %i[spec rubocop:auto_correct]
@@ -4,16 +4,15 @@ require 'optparse'
4
4
  require 'restforce'
5
5
  require 'yaml'
6
6
  require 'forwardable'
7
+ require 'dry-initializer'
7
8
 
8
9
  require 'salesforce_streamer/configuration'
9
10
  require 'salesforce_streamer/errors'
10
11
  require 'salesforce_streamer/replay_id_error_extension'
11
12
  require 'salesforce_streamer/log'
12
13
  require 'salesforce_streamer/push_topic'
13
- require 'salesforce_streamer/topic_manager'
14
+ require 'salesforce_streamer/salesforce_topic_manager'
14
15
  require 'salesforce_streamer/salesforce_client'
15
- require 'salesforce_streamer/replay_persistence'
16
- require 'salesforce_streamer/redis_replay'
17
16
  require 'salesforce_streamer/server'
18
17
  require 'salesforce_streamer/version'
19
18
  require 'salesforce_streamer/launcher'
@@ -60,9 +59,7 @@ module SalesforceStreamer
60
59
  Configuration.instance
61
60
  end
62
61
 
63
- class RedisConnectionError < StandardError
64
- def initialize
65
- super 'SalesforceStreamer.config.redis_connection not set'
66
- end
62
+ def self.configure
63
+ yield Configuration.instance
67
64
  end
68
65
  end
@@ -32,7 +32,7 @@ module SalesforceStreamer
32
32
  end
33
33
 
34
34
  o.on '-v', '--verbose LEVEL', 'Set the log level (default no logging)' do |arg|
35
- @config.logger = Logger.new(STDERR, level: arg)
35
+ @config.logger = Logger.new($stderr, level: arg)
36
36
  end
37
37
 
38
38
  o.on '-V', '--version', 'Print the version information' do
@@ -1,22 +1,27 @@
1
1
  module SalesforceStreamer
2
2
  # Manages server configuration.
3
3
  class Configuration
4
- attr_accessor :environment, :logger, :require_path, :config_file, :manage_topics,
5
- :server, :exception_adapter, :persistence_adapter, :redis_connection, :middleware
4
+ attr_accessor :environment, :logger, :require_path, :config_file,
5
+ :manage_topics, :exception_adapter, :replay_adapter
6
+ attr_reader :middleware
6
7
 
7
8
  class << self
8
9
  attr_writer :instance
9
- end
10
10
 
11
- def self.instance
12
- @instance ||= new
11
+ def configure
12
+ yield instance
13
+ end
14
+
15
+ def instance
16
+ @instance ||= new
17
+ end
13
18
  end
14
19
 
15
20
  def initialize
16
21
  @environment = ENV['RACK_ENV'] || :development
17
22
  @logger = Logger.new(IO::NULL)
18
23
  @exception_adapter = proc { |exc| fail(exc) }
19
- @persistence_adapter = RedisReplay.new
24
+ @replay_adapter = Hash.new { |hash, key| hash[key] = -1 }
20
25
  @manage_topics = false
21
26
  @config_file = './config/streamer.yml'
22
27
  @require_path = './config/environment'
@@ -27,12 +32,17 @@ module SalesforceStreamer
27
32
  @manage_topics
28
33
  end
29
34
 
35
+ # adds a setup proc to the middleware array
30
36
  def use_middleware(klass, *args, &block)
31
- middleware << proc { |app| klass.new(app, *args, &block) }
37
+ @middleware << [klass, args, block]
32
38
  end
33
39
 
34
- def middleware_chain_for(app)
35
- middleware.reduce(app) { |memo, middleware| middleware.call(memo) }
40
+ # returns a ready to use chain of middleware
41
+ def middleware_runner(last_handler)
42
+ @middleware.reduce(last_handler) do |next_handler, current_handler|
43
+ klass, args, block = current_handler
44
+ klass.new(next_handler, *args, &block)
45
+ end
36
46
  end
37
47
 
38
48
  def push_topic_data
@@ -3,13 +3,13 @@ module SalesforceStreamer
3
3
 
4
4
  class MissingCLIFlagError < StandardError
5
5
  def initialize(flag)
6
- super 'Missing required command line flag: ' + flag.to_s
6
+ super "Missing required command line flag: #{flag}"
7
7
  end
8
8
  end
9
9
 
10
10
  class NilQueryError < StandardError
11
11
  def initialize(name)
12
- super 'Query not defined for ' + name.to_s
12
+ super "Query not defined for #{name}"
13
13
  end
14
14
  end
15
15
 
@@ -21,7 +21,13 @@ module SalesforceStreamer
21
21
 
22
22
  class PushTopicNameTooLongError < StandardError
23
23
  def initialize(name)
24
- super 'PushTopic name: ' + name.to_s + ' (' + name.size.to_s + '/25)'
24
+ super "PushTopic name: #{name} (#{name.size}/25)"
25
+ end
26
+ end
27
+
28
+ class UnprocessableHandlerError < StandardError
29
+ def initialize(constant)
30
+ super "#{constant} does not repond to .call or .perform_async"
25
31
  end
26
32
  end
27
33
  end
@@ -5,14 +5,14 @@ module SalesforceStreamer
5
5
  class Launcher
6
6
  def initialize
7
7
  load_server_configuration
8
- @manager = TopicManager.new push_topics: @push_topics
8
+ @manager = SalesforceTopicManager.new push_topics: @push_topics
9
9
  @server = Server.new push_topics: @push_topics
10
10
  end
11
11
 
12
12
  # Manages each PushTopic configured and starts the Streaming API listener.
13
13
  def run
14
14
  Log.info 'Launching Streamer Services'
15
- @manager.run
15
+ @manager.upsert_topics!
16
16
  @server.push_topics = @manager.push_topics
17
17
  @server.run
18
18
  end
@@ -36,7 +36,7 @@ module SalesforceStreamer
36
36
  @push_topics = []
37
37
  Configuration.instance.push_topic_data.each_value do |topic_data|
38
38
  Log.debug topic_data.to_s
39
- @push_topics << PushTopic.new(data: topic_data)
39
+ @push_topics << PushTopic.new(topic_data.transform_keys(&:to_sym))
40
40
  end
41
41
  end
42
42
  end
@@ -1,38 +1,29 @@
1
1
  module SalesforceStreamer
2
2
  # Models the PushTopic object for both Restforce and Streamer
3
3
  class PushTopic
4
- attr_accessor :id
5
- attr_reader :name, :description, :notify_for_fields, :query,
6
- :handler, :handler_constant, :api_version
4
+ extend Dry::Initializer
7
5
 
8
- def initialize(data:)
9
- @handler = data['handler']
10
- @static_replay = data.dig('replay')&.to_i || -1
11
- @name = data.dig('salesforce', 'name')
12
- @api_version = data.dig('salesforce', 'api_version') || '41.0'
13
- @description = data.dig('salesforce', 'description') || @name
14
- @notify_for_fields = data.dig('salesforce', 'notify_for_fields') || 'Referenced'
15
- @query = strip_spaces(data.dig('salesforce', 'query'))
16
- validate!
17
- end
6
+ option :name
7
+ option :query, proc { |str| str.gsub(/\s+/, ' ') }
8
+ option :handler, proc { |str| prepare_handler_proc Object.const_get(str) }
9
+ option :replay, proc(&:to_i), default: proc { -1 }
10
+ option :api_version, proc(&:to_s), default: proc { '49.0' }
11
+ option :notify_for_fields, default: proc { 'Referenced' }
12
+ option :id, optional: true
13
+ option :description, optional: true
18
14
 
19
- def replay
20
- ReplayPersistence.retrieve(name) || @static_replay
21
- end
15
+ attr_writer :id
22
16
 
23
17
  def handle(message)
24
18
  message['topic'] = @name
25
- handle_chain.call(message)
26
- ReplayPersistence.record @name, message.dig('event', 'replayId')
19
+ message_middleware.call(message)
27
20
  rescue StandardError => e
28
21
  Log.error e
29
22
  Configuration.instance.exception_adapter.call e
30
23
  end
31
24
 
32
- def to_s
33
- "PushTopic id=#{id} name=#{name} handler=#{handler} " \
34
- "replay=#{replay} notify_for_fields=#{notify_for_fields} " \
35
- "description=#{description} api_version=#{api_version} query=#{query}"
25
+ def attributes
26
+ self.class.dry_initializer.public_attributes self
36
27
  end
37
28
 
38
29
  private
@@ -40,29 +31,33 @@ module SalesforceStreamer
40
31
  def validate!
41
32
  fail(PushTopicNameTooLongError, @name) if @name.size > 25
42
33
 
43
- @handler_constant = Object.const_get(@handler)
34
+ @handler = Object.const_get(@handler)
44
35
  true
45
36
  rescue NameError, TypeError => e
46
- message = 'handler=' + @handler.to_s + ' exception=' + e.to_s
37
+ message = "handler=#{@handler} exception=#{e}"
47
38
  raise(PushTopicHandlerMissingError, message)
48
39
  end
49
40
 
50
- def handle_chain
51
- Configuration.instance.middleware_chain_for(handler_proc)
41
+ def message_middleware
42
+ Configuration.instance.middleware_runner(handler)
52
43
  end
53
44
 
54
- def handler_proc
55
- if handler_constant.respond_to? :perform_async
56
- proc { |message| handler_constant.perform_async message }
57
- else
58
- handler_constant
59
- end
60
- end
45
+ class << self
46
+ def strip_spaces(str)
47
+ fail(NilQueryError, @name) unless str
61
48
 
62
- def strip_spaces(str)
63
- fail(NilQueryError, @name) unless str
49
+ str.gsub(/\s+/, ' ')
50
+ end
64
51
 
65
- str.gsub(/\s+/, ' ')
52
+ def prepare_handler_proc(constant)
53
+ if constant.respond_to? :call
54
+ constant
55
+ elsif constant.respond_to? :perform_async
56
+ proc { |message| constant.perform_async message }
57
+ else
58
+ fail(UnprocessableHandlerError, constant)
59
+ end
60
+ end
66
61
  end
67
62
  end
68
63
  end
@@ -8,10 +8,8 @@ module SalesforceStreamer
8
8
  @client.authenticate!
9
9
  end
10
10
 
11
- def subscribe(*args)
12
- @client.subscribe(args) do
13
- yield
14
- end
11
+ def subscribe(*args, &block)
12
+ @client.subscribe(args, &block)
15
13
  end
16
14
 
17
15
  # Returns nil or an instance of Restforce::SObject
@@ -1,5 +1,5 @@
1
1
  module SalesforceStreamer
2
- class TopicManager
2
+ class SalesforceTopicManager
3
3
  attr_reader :push_topics
4
4
 
5
5
  def initialize(push_topics:)
@@ -7,10 +7,11 @@ module SalesforceStreamer
7
7
  @client = SalesforceClient.new
8
8
  end
9
9
 
10
- def run
11
- Log.info 'Running Topic Manager'
10
+ def upsert_topics!
11
+ Log.info 'Starting to upsert PushTopic definitions into Salesforce'
12
12
  @push_topics.each do |push_topic|
13
- Log.debug push_topic.to_s
13
+ Log.info push_topic.name
14
+ Log.debug push_topic.attributes.to_json
14
15
  upsert(push_topic) if diff?(push_topic)
15
16
  end
16
17
  end
@@ -29,12 +30,12 @@ module SalesforceStreamer
29
30
  return true unless push_topic.notify_for_fields.eql?(hashie.NotifyForFields)
30
31
  return true unless push_topic.api_version.to_s.eql?(hashie.ApiVersion.to_s)
31
32
 
32
- Log.debug 'No differences detected'
33
+ Log.info 'No differences detected'
33
34
  false
34
35
  end
35
36
 
36
37
  def upsert(push_topic)
37
- Log.info "Upsert PushTopic #{push_topic.name}"
38
+ Log.info "Upserting PushTopic"
38
39
  if Configuration.instance.manage_topics?
39
40
  @client.upsert_push_topic(push_topic)
40
41
  else
@@ -34,11 +34,9 @@ module SalesforceStreamer
34
34
  def start_em
35
35
  EM.run do
36
36
  @push_topics.map do |topic|
37
- client.subscribe topic.name, replay: topic.replay.to_i do |message|
38
- replay_id = message.dig('event', 'replayId')
39
- Log.info "Message #{replay_id} received from topic #{topic.name}"
37
+ client.subscribe topic.name, replay: Configuration.instance.replay_adapter do |message|
38
+ Log.info "Message #{message.dig('event', 'replayId')} received from topic #{topic.name}"
40
39
  topic.handle message
41
- topic.id = replay_id
42
40
  end
43
41
  end
44
42
  end
@@ -1,3 +1,3 @@
1
1
  module SalesforceStreamer
2
- VERSION = '1.2.2'.freeze
2
+ VERSION = '2.1.1'.freeze
3
3
  end
@@ -14,6 +14,7 @@ Gem::Specification.new do |spec|
14
14
 
15
15
  spec.metadata['homepage_uri'] = spec.homepage
16
16
  spec.metadata['source_code_uri'] = spec.homepage
17
+ spec.metadata['documentation_uri'] = 'https://www.rubydoc.info/gems/salesforce_streamer'
17
18
 
18
19
  spec.files = Dir.chdir(File.expand_path(__dir__)) do
19
20
  `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(bin/|spec/|\.rub)}) }
@@ -24,8 +25,9 @@ Gem::Specification.new do |spec|
24
25
 
25
26
  spec.required_ruby_version = '>= 2.6'
26
27
 
27
- spec.add_dependency 'faye', '0.8.9'
28
- spec.add_dependency 'restforce', '~> 4.2'
28
+ spec.add_dependency 'dry-initializer', '~> 3.0'
29
+ spec.add_dependency 'faye', '~> 1.4'
30
+ spec.add_dependency 'restforce', '>= 4.2', '< 6.0'
29
31
 
30
32
  spec.add_development_dependency 'byebug'
31
33
  spec.add_development_dependency 'codecov'
metadata CHANGED
@@ -1,44 +1,64 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: salesforce_streamer
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.2
4
+ version: 2.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Scott Serok
8
8
  - RenoFi Engineering Team
9
- autorequire:
9
+ autorequire:
10
10
  bindir: exe
11
11
  cert_chain: []
12
- date: 2020-08-03 00:00:00.000000000 Z
12
+ date: 2020-09-02 00:00:00.000000000 Z
13
13
  dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: dry-initializer
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - "~>"
19
+ - !ruby/object:Gem::Version
20
+ version: '3.0'
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - "~>"
26
+ - !ruby/object:Gem::Version
27
+ version: '3.0'
14
28
  - !ruby/object:Gem::Dependency
15
29
  name: faye
16
30
  requirement: !ruby/object:Gem::Requirement
17
31
  requirements:
18
- - - '='
32
+ - - "~>"
19
33
  - !ruby/object:Gem::Version
20
- version: 0.8.9
34
+ version: '1.4'
21
35
  type: :runtime
22
36
  prerelease: false
23
37
  version_requirements: !ruby/object:Gem::Requirement
24
38
  requirements:
25
- - - '='
39
+ - - "~>"
26
40
  - !ruby/object:Gem::Version
27
- version: 0.8.9
41
+ version: '1.4'
28
42
  - !ruby/object:Gem::Dependency
29
43
  name: restforce
30
44
  requirement: !ruby/object:Gem::Requirement
31
45
  requirements:
32
- - - "~>"
46
+ - - ">="
33
47
  - !ruby/object:Gem::Version
34
48
  version: '4.2'
49
+ - - "<"
50
+ - !ruby/object:Gem::Version
51
+ version: '6.0'
35
52
  type: :runtime
36
53
  prerelease: false
37
54
  version_requirements: !ruby/object:Gem::Requirement
38
55
  requirements:
39
- - - "~>"
56
+ - - ">="
40
57
  - !ruby/object:Gem::Version
41
58
  version: '4.2'
59
+ - - "<"
60
+ - !ruby/object:Gem::Version
61
+ version: '6.0'
42
62
  - !ruby/object:Gem::Dependency
43
63
  name: byebug
44
64
  requirement: !ruby/object:Gem::Requirement
@@ -137,7 +157,7 @@ dependencies:
137
157
  - - ">="
138
158
  - !ruby/object:Gem::Version
139
159
  version: '0'
140
- description:
160
+ description:
141
161
  email:
142
162
  - scott@renofi.com
143
163
  - engineering@renofi.com
@@ -155,6 +175,7 @@ files:
155
175
  - ".gitignore"
156
176
  - ".rspec"
157
177
  - ".travis.yml"
178
+ - CHANGELOG.md
158
179
  - Gemfile
159
180
  - Gemfile.lock
160
181
  - LICENSE.txt
@@ -169,12 +190,10 @@ files:
169
190
  - lib/salesforce_streamer/launcher.rb
170
191
  - lib/salesforce_streamer/log.rb
171
192
  - lib/salesforce_streamer/push_topic.rb
172
- - lib/salesforce_streamer/redis_replay.rb
173
193
  - lib/salesforce_streamer/replay_id_error_extension.rb
174
- - lib/salesforce_streamer/replay_persistence.rb
175
194
  - lib/salesforce_streamer/salesforce_client.rb
195
+ - lib/salesforce_streamer/salesforce_topic_manager.rb
176
196
  - lib/salesforce_streamer/server.rb
177
- - lib/salesforce_streamer/topic_manager.rb
178
197
  - lib/salesforce_streamer/version.rb
179
198
  - salesforce_streamer.gemspec
180
199
  homepage: https://github.com/renofi/salesforce_streamer
@@ -183,7 +202,8 @@ licenses:
183
202
  metadata:
184
203
  homepage_uri: https://github.com/renofi/salesforce_streamer
185
204
  source_code_uri: https://github.com/renofi/salesforce_streamer
186
- post_install_message:
205
+ documentation_uri: https://www.rubydoc.info/gems/salesforce_streamer
206
+ post_install_message:
187
207
  rdoc_options: []
188
208
  require_paths:
189
209
  - lib
@@ -199,7 +219,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
199
219
  version: '0'
200
220
  requirements: []
201
221
  rubygems_version: 3.1.2
202
- signing_key:
222
+ signing_key:
203
223
  specification_version: 4
204
224
  summary: A wrapper around the Restforce Streaming API with a built-in PushTopic manager.
205
225
  test_files: []
@@ -1,55 +0,0 @@
1
- module SalesforceStreamer
2
- class RedisReplay
3
- class << self
4
- def redis_connection
5
- @redis_connection ||= Configuration.instance.redis_connection || fail(RedisConnectionError)
6
- end
7
-
8
- attr_writer :redis_connection
9
- end
10
-
11
- def connection
12
- if RedisReplay.redis_connection.respond_to?(:with)
13
- RedisReplay.redis_connection.with do |conn|
14
- yield(conn)
15
- end
16
- else
17
- yield RedisReplay.redis_connection
18
- end
19
- end
20
-
21
- # Saves the value to a key with expiration
22
- def record(key, value)
23
- return unless key && value
24
-
25
- key = namespaced_key(key)
26
- value = Integer(value)
27
- connection { |c| c.setex key, SECONDS_TO_EXPIRE, value }
28
- rescue StandardError => e
29
- Configuration.instance.exception_adapter.call e
30
- nil
31
- end
32
-
33
- def retrieve(key)
34
- return unless key
35
-
36
- key = namespaced_key(key)
37
- value = connection { |c| c.get key }
38
- Integer(value) if value
39
- rescue StandardError => e
40
- Configuration.instance.exception_adapter.call e
41
- nil
42
- end
43
-
44
- private
45
-
46
- def namespaced_key(key)
47
- NAMESPACE + key.to_s
48
- end
49
-
50
- NAMESPACE = 'SalesforceStreamer:'.freeze
51
- SECONDS_TO_EXPIRE = 24 * 60 * 60 # 24 hours
52
- START = 0
53
- STOP = 0
54
- end
55
- end
@@ -1,18 +0,0 @@
1
- module SalesforceStreamer
2
- # Store values for a given key in a sorted sequence
3
- # Retrieves the highest value given a key
4
- class ReplayPersistence
5
- class << self
6
- def record(key, value)
7
- Log.debug { "Recording #{key}=#{value}" }
8
- Configuration.instance.persistence_adapter&.record key, value
9
- end
10
-
11
- def retrieve(key)
12
- Configuration.instance.persistence_adapter&.retrieve(key).tap do |v|
13
- Log.debug { "Retrieved for #{key} #{v || 'nil'}" }
14
- end
15
- end
16
- end
17
- end
18
- end