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 +4 -4
- data/.dependabot/config.yml +0 -7
- data/CHANGELOG.md +99 -0
- data/Gemfile.lock +26 -26
- data/README.md +62 -48
- data/Rakefile +1 -1
- data/lib/salesforce_streamer.rb +4 -7
- data/lib/salesforce_streamer/cli.rb +1 -1
- data/lib/salesforce_streamer/configuration.rb +19 -9
- data/lib/salesforce_streamer/errors.rb +9 -3
- data/lib/salesforce_streamer/launcher.rb +3 -3
- data/lib/salesforce_streamer/push_topic.rb +31 -36
- data/lib/salesforce_streamer/salesforce_client.rb +2 -4
- data/lib/salesforce_streamer/{topic_manager.rb → salesforce_topic_manager.rb} +7 -6
- data/lib/salesforce_streamer/server.rb +2 -4
- data/lib/salesforce_streamer/version.rb +1 -1
- data/salesforce_streamer.gemspec +4 -2
- metadata +35 -15
- data/lib/salesforce_streamer/redis_replay.rb +0 -55
- data/lib/salesforce_streamer/replay_persistence.rb +0 -18
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4a92bbc0eaffa8152b8cdcc6b8eb35248a8fe1e996bc73410dc1ed4043acde08
|
4
|
+
data.tar.gz: '0787479213ab7bc07c07a6cc5d8c9034f5593aee613a28529e25dd51e8c3d9c2'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 95e9b7ba4133fe7c7840e413677cff3121b6d4c94c9f1037b4fd91fe1a111dd6a1ac350075b987d68cd1fbcb72b733fbed3bd087353b6a075789a8fd15c014cb
|
7
|
+
data.tar.gz: 573892b3222c3240fb8d171b32f03f446bae3a0ff2184aa7bbbdf71f940ecc961cd84a634dda824a9ecb42acb13446e44b8d0d8f4f4dacb4cae0f1c555658cf0
|
data/.dependabot/config.yml
CHANGED
data/CHANGELOG.md
ADDED
@@ -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.
|
data/Gemfile.lock
CHANGED
@@ -1,9 +1,10 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
salesforce_streamer (1.
|
5
|
-
|
6
|
-
|
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.
|
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
|
-
|
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.
|
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 (
|
36
|
+
faye (1.4.0)
|
37
37
|
cookiejar (>= 0.3.0)
|
38
|
-
em-http-request (>=
|
38
|
+
em-http-request (>= 1.1.6)
|
39
39
|
eventmachine (>= 0.12.0)
|
40
|
-
faye-websocket (>= 0.
|
40
|
+
faye-websocket (>= 0.11.0)
|
41
|
+
multi_json (>= 1.0.0)
|
41
42
|
rack (>= 1.0.0)
|
42
|
-
|
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.
|
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 (
|
60
|
-
faraday (>= 0.9.0, <=
|
61
|
-
faraday_middleware (>= 0.8.8, <=
|
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.
|
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.
|
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.
|
89
|
-
parser (>= 2.7.
|
90
|
-
rubocop-performance (1.7.
|
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.
|
93
|
-
rubocop (
|
93
|
+
rubocop-rspec (1.43.2)
|
94
|
+
rubocop (~> 0.87)
|
94
95
|
ruby-progressbar (1.10.1)
|
95
|
-
simplecov (0.
|
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
|
46
|
-
|
47
|
-
|
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
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
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
|
-
|
93
|
-
|
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
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
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
data/lib/salesforce_streamer.rb
CHANGED
@@ -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/
|
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
|
-
|
64
|
-
|
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(
|
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,
|
5
|
-
:
|
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
|
-
|
12
|
-
|
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
|
-
@
|
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 <<
|
37
|
+
@middleware << [klass, args, block]
|
32
38
|
end
|
33
39
|
|
34
|
-
|
35
|
-
|
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
|
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
|
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
|
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 =
|
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.
|
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(
|
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
|
-
|
5
|
-
attr_reader :name, :description, :notify_for_fields, :query,
|
6
|
-
:handler, :handler_constant, :api_version
|
4
|
+
extend Dry::Initializer
|
7
5
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
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
|
-
|
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
|
-
|
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
|
33
|
-
|
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
|
-
@
|
34
|
+
@handler = Object.const_get(@handler)
|
44
35
|
true
|
45
36
|
rescue NameError, TypeError => e
|
46
|
-
message =
|
37
|
+
message = "handler=#{@handler} exception=#{e}"
|
47
38
|
raise(PushTopicHandlerMissingError, message)
|
48
39
|
end
|
49
40
|
|
50
|
-
def
|
51
|
-
Configuration.instance.
|
41
|
+
def message_middleware
|
42
|
+
Configuration.instance.middleware_runner(handler)
|
52
43
|
end
|
53
44
|
|
54
|
-
|
55
|
-
|
56
|
-
|
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
|
-
|
63
|
-
|
49
|
+
str.gsub(/\s+/, ' ')
|
50
|
+
end
|
64
51
|
|
65
|
-
|
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)
|
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
|
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
|
11
|
-
Log.info '
|
10
|
+
def upsert_topics!
|
11
|
+
Log.info 'Starting to upsert PushTopic definitions into Salesforce'
|
12
12
|
@push_topics.each do |push_topic|
|
13
|
-
Log.
|
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.
|
33
|
+
Log.info 'No differences detected'
|
33
34
|
false
|
34
35
|
end
|
35
36
|
|
36
37
|
def upsert(push_topic)
|
37
|
-
Log.info "
|
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:
|
38
|
-
|
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
|
data/salesforce_streamer.gemspec
CHANGED
@@ -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 '
|
28
|
-
spec.add_dependency '
|
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.
|
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-
|
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:
|
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:
|
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
|
-
|
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
|