salesforce_streamer 1.2.1 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b143e33e82a48eab99336814e577ea28f3587ff001215d0f113cde9ba4cd4af9
4
- data.tar.gz: 0e14143ff38b5cea8aaf98ff1232d50c9240e9867cee9e7a9e8931b11b519875
3
+ metadata.gz: b7aa51a1aab875d18d027a1686a598d795d30126cea6fc81f08e705911bdc92c
4
+ data.tar.gz: ce9f47fdc1f3c277347c684fc533e92e56e93779c3115a0894f8834c1196b795
5
5
  SHA512:
6
- metadata.gz: d8ddac32a872d67fc643e5b917e3f9a6226cb36baea76a256ebba6c74f29414a8f66d403cee40f440fd8876da17af8fcc72c169e2a4e234679dd1b071a59a537
7
- data.tar.gz: ee20957061503d2301a4044429dd86966b6c57f3b9277ce00d5c6a471c7501cb266a1be7afb71ff66f0f7d2c69d98d6a5d9032fdd39d052214cdeafb27c8ce42
6
+ metadata.gz: 6d7183472e280c49188c8f0a386ddf24e47ee99cd827b0e1b9bb3c22184136ef81694364020a4c6676f728317af530e5959908fe79d871ba065d43f29b0c5f97
7
+ data.tar.gz: 329f63f5ced7e03256761765e70a3d13107779fa895e249e69e07e98d148ed1142b0f23c9e8cc86d38faae92087af59080524cef60a70ba51619ab4ec9cdd36b
@@ -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,33 @@
1
+ ---
2
+ name: Bug report
3
+ about: Create a report to track an issue that has been identified
4
+ title: ''
5
+ labels: ''
6
+ assignees: ''
7
+
8
+ ---
9
+
10
+ **Describe the bug**
11
+ A clear and concise description of what the bug is.
12
+
13
+ **To Reproduce**
14
+ Steps to reproduce the behavior:
15
+ 1. Go to '...'
16
+ 2. Click on '....'
17
+ 3. Scroll down to '....'
18
+ 4. See error
19
+
20
+ **Expected behavior**
21
+ A clear and concise description of what you expected to happen.
22
+
23
+ **Mutation/Query**
24
+
25
+ **URL and HTTP method (for non-GQL):**
26
+
27
+ **Sentry or Logs URL:**
28
+
29
+ **User/authentication details**
30
+ Impacted user name or service account
31
+
32
+ **Additional context**
33
+ Add any other context about the problem here.
@@ -0,0 +1 @@
1
+ blank_issues_enabled: false
@@ -0,0 +1,22 @@
1
+ ---
2
+ name: New story
3
+ about: Add a new story for implementation
4
+ title: ''
5
+ labels: ''
6
+ assignees: ''
7
+
8
+ ---
9
+
10
+ **Describe the solution**
11
+ A clear and concise description of what you want to happen.
12
+ When will this feature be done?
13
+
14
+ **Describe the users**
15
+ Who are we building this feature for?
16
+
17
+ **Additional context**
18
+ Add any other context or screenshots about the feature request here.
19
+ Link to any applicable documents describing the feature.
20
+
21
+ **Designs**
22
+ Link to any applicable designs on Invision.
@@ -0,0 +1,18 @@
1
+ ## Description
2
+ <!--- Describe your changes in detail -->
3
+
4
+ ## Related issue(s)
5
+ <!--- GH issue number -->
6
+
7
+ ## Motivation and Context
8
+ <!--- Why is this change required? What problem does it solve? -->
9
+ <!--- If it fixes an open issue, please link to the issue here. -->
10
+
11
+ ## How Has This Been Tested?
12
+ <!--- Please describe in detail how you tested your changes. -->
13
+
14
+ ## Screenshots (if appropriate):
15
+ <!--- Please add any screenshots of the feature. -->
16
+
17
+ ## Related PRs
18
+ <!--- Please add links to any related PRs (backend, component packages, etc). -->
@@ -0,0 +1,16 @@
1
+ # automatically approve PRs submitted by Dependabot
2
+ # this will allow Dependabot to automatically merge dependency update PRs where CI passes
3
+ # from: https://github.com/hmarr/auto-approve-action
4
+ name: Auto approve Dependabot PRs
5
+
6
+ on:
7
+ pull_request
8
+
9
+ jobs:
10
+ auto-approve:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: hmarr/auto-approve-action@v2.0.0
14
+ if: github.actor == 'dependabot[bot]' || github.actor == 'dependabot-preview[bot]'
15
+ with:
16
+ github-token: "${{ secrets.GITHUB_TOKEN }}"
@@ -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,25 +1,28 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- salesforce_streamer (1.2.1)
5
- faye (= 0.8.9)
6
- restforce (~> 4.2)
4
+ salesforce_streamer (2.1.0)
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/
10
11
  specs:
11
12
  addressable (2.7.0)
12
13
  public_suffix (>= 2.0.2, < 5.0)
13
- ast (2.4.0)
14
+ ast (2.4.1)
14
15
  byebug (11.1.3)
15
- codecov (0.1.16)
16
+ codecov (0.2.3)
17
+ colorize
16
18
  json
17
19
  simplecov
18
- url
20
+ colorize (0.8.1)
19
21
  cookiejar (0.3.3)
20
- diff-lcs (1.3)
22
+ diff-lcs (1.4.4)
21
23
  docile (1.3.2)
22
- em-http-request (1.1.5)
24
+ dry-initializer (3.0.3)
25
+ em-http-request (1.1.6)
23
26
  addressable (>= 2.3.4)
24
27
  cookiejar (!= 0.3.1)
25
28
  em-socksify (>= 0.3)
@@ -28,38 +31,39 @@ GEM
28
31
  em-socksify (0.3.2)
29
32
  eventmachine (>= 1.0.0.beta.4)
30
33
  eventmachine (1.2.7)
31
- faraday (1.0.0)
34
+ faraday (1.0.1)
32
35
  multipart-post (>= 1.2, < 3)
33
36
  faraday_middleware (1.0.0)
34
37
  faraday (~> 1.0)
35
- faye (0.8.9)
38
+ faye (1.4.0)
36
39
  cookiejar (>= 0.3.0)
37
- em-http-request (>= 0.3.0)
40
+ em-http-request (>= 1.1.6)
38
41
  eventmachine (>= 0.12.0)
39
- faye-websocket (>= 0.4.0)
42
+ faye-websocket (>= 0.11.0)
43
+ multi_json (>= 1.0.0)
40
44
  rack (>= 1.0.0)
41
- yajl-ruby (>= 1.0.0)
42
- faye-websocket (0.10.9)
45
+ websocket-driver (>= 0.5.1)
46
+ faye-websocket (0.11.0)
43
47
  eventmachine (>= 0.12.0)
44
48
  websocket-driver (>= 0.5.1)
45
49
  hashie (4.1.0)
46
50
  http_parser.rb (0.6.0)
47
- jaro_winkler (1.5.4)
48
- json (2.3.0)
51
+ json (2.3.1)
49
52
  jwt (2.2.1)
53
+ multi_json (1.15.0)
50
54
  multipart-post (2.1.1)
51
- parallel (1.19.1)
52
- parser (2.7.1.2)
53
- ast (~> 2.4.0)
55
+ parallel (1.19.2)
56
+ parser (2.7.1.4)
57
+ ast (~> 2.4.1)
54
58
  public_suffix (4.0.5)
55
- rack (2.2.2)
59
+ rack (2.2.3)
56
60
  rainbow (3.0.0)
57
61
  rake (13.0.1)
58
- restforce (4.2.2)
59
- faraday (>= 0.9.0, <= 1.0)
60
- faraday_middleware (>= 0.8.8, <= 1.0)
62
+ regexp_parser (1.7.1)
63
+ restforce (5.0.1)
64
+ faraday (>= 0.9.0, <= 2.0)
65
+ faraday_middleware (>= 0.8.8, <= 2.0)
61
66
  hashie (>= 1.2.0, < 5.0)
62
- json (>= 1.7.5)
63
67
  jwt (>= 1.5.6)
64
68
  rexml (3.2.4)
65
69
  rspec (3.9.0)
@@ -75,29 +79,30 @@ GEM
75
79
  diff-lcs (>= 1.2.0, < 2.0)
76
80
  rspec-support (~> 3.9.0)
77
81
  rspec-support (3.9.3)
78
- rubocop (0.82.0)
79
- jaro_winkler (~> 1.5.1)
82
+ rubocop (0.88.0)
80
83
  parallel (~> 1.10)
81
- parser (>= 2.7.0.1)
84
+ parser (>= 2.7.1.1)
82
85
  rainbow (>= 2.2.2, < 4.0)
86
+ regexp_parser (>= 1.7)
83
87
  rexml
88
+ rubocop-ast (>= 0.1.0, < 1.0)
84
89
  ruby-progressbar (~> 1.7)
85
90
  unicode-display_width (>= 1.4.0, < 2.0)
86
- rubocop-performance (1.5.2)
87
- rubocop (>= 0.71.0)
88
- rubocop-rspec (1.39.0)
89
- rubocop (>= 0.68.1)
91
+ rubocop-ast (0.3.0)
92
+ parser (>= 2.7.1.4)
93
+ rubocop-performance (1.7.1)
94
+ rubocop (>= 0.82.0)
95
+ rubocop-rspec (1.42.0)
96
+ rubocop (>= 0.87.0)
90
97
  ruby-progressbar (1.10.1)
91
98
  simplecov (0.18.5)
92
99
  docile (~> 1.1)
93
100
  simplecov-html (~> 0.11)
94
101
  simplecov-html (0.12.2)
95
102
  unicode-display_width (1.7.0)
96
- url (0.3.2)
97
- websocket-driver (0.7.1)
103
+ websocket-driver (0.7.3)
98
104
  websocket-extensions (>= 0.1.0)
99
- websocket-extensions (0.1.4)
100
- yajl-ruby (1.4.1)
105
+ websocket-extensions (0.1.5)
101
106
 
102
107
  PLATFORMS
103
108
  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]
@@ -9,7 +9,7 @@ module CoreExtensions
9
9
  def self.extended(base)
10
10
  base.class_eval do
11
11
  def self.domains_match(tested_domain, base_domain)
12
- return true if tested_domain[-15..-1].eql?('.salesforce.com')
12
+ return true if tested_domain[-15..].eql?('.salesforce.com')
13
13
 
14
14
  # original implementation
15
15
  base = effective_host base_domain
@@ -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.1'.freeze
2
+ VERSION = '2.1.0'.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,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: salesforce_streamer
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.1
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Scott Serok
@@ -9,36 +9,56 @@ authors:
9
9
  autorequire:
10
10
  bindir: exe
11
11
  cert_chain: []
12
- date: 2020-05-11 00:00:00.000000000 Z
12
+ date: 2020-08-18 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
@@ -147,9 +167,15 @@ extensions: []
147
167
  extra_rdoc_files: []
148
168
  files:
149
169
  - ".dependabot/config.yml"
170
+ - ".github/ISSUE_TEMPLATE/bug_report.md"
171
+ - ".github/ISSUE_TEMPLATE/config.yml"
172
+ - ".github/ISSUE_TEMPLATE/story.md"
173
+ - ".github/PULL_REQUEST_TEMPLATE.md"
174
+ - ".github/workflows/auto-approve.yml"
150
175
  - ".gitignore"
151
176
  - ".rspec"
152
177
  - ".travis.yml"
178
+ - CHANGELOG.md
153
179
  - Gemfile
154
180
  - Gemfile.lock
155
181
  - LICENSE.txt
@@ -164,12 +190,10 @@ files:
164
190
  - lib/salesforce_streamer/launcher.rb
165
191
  - lib/salesforce_streamer/log.rb
166
192
  - lib/salesforce_streamer/push_topic.rb
167
- - lib/salesforce_streamer/redis_replay.rb
168
193
  - lib/salesforce_streamer/replay_id_error_extension.rb
169
- - lib/salesforce_streamer/replay_persistence.rb
170
194
  - lib/salesforce_streamer/salesforce_client.rb
195
+ - lib/salesforce_streamer/salesforce_topic_manager.rb
171
196
  - lib/salesforce_streamer/server.rb
172
- - lib/salesforce_streamer/topic_manager.rb
173
197
  - lib/salesforce_streamer/version.rb
174
198
  - salesforce_streamer.gemspec
175
199
  homepage: https://github.com/renofi/salesforce_streamer
@@ -178,6 +202,7 @@ licenses:
178
202
  metadata:
179
203
  homepage_uri: https://github.com/renofi/salesforce_streamer
180
204
  source_code_uri: https://github.com/renofi/salesforce_streamer
205
+ documentation_uri: https://www.rubydoc.info/gems/salesforce_streamer
181
206
  post_install_message:
182
207
  rdoc_options: []
183
208
  require_paths:
@@ -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