restforce 0.0.8 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of restforce might be problematic. Click here for more details.
- data/README.md +54 -18
- data/lib/restforce.rb +3 -0
- data/lib/restforce/client.rb +40 -14
- data/lib/restforce/config.rb +6 -2
- data/lib/restforce/middleware/authentication.rb +9 -4
- data/lib/restforce/middleware/caching.rb +8 -0
- data/lib/restforce/version.rb +1 -1
- data/spec/lib/client_spec.rb +58 -20
- data/spec/lib/config_spec.rb +3 -2
- data/spec/lib/middleware/authentication_spec.rb +14 -1
- metadata +2 -2
data/README.md
CHANGED
@@ -12,6 +12,7 @@ It attempts to solve a couple of key issues that the databasedotcom gem has been
|
|
12
12
|
* Support for the Streaming API
|
13
13
|
* Support for blob data types.
|
14
14
|
* A clean and modular architecture using [Faraday middleware](https://github.com/technoweenie/faraday)
|
15
|
+
* Support for decoding [Force.com Canvas](http://www.salesforce.com/us/developer/docs/platform_connectpre/canvas_framework.pdf) signed requests. (NEW!)
|
15
16
|
|
16
17
|
[Documentation](http://rubydoc.info/gems/restforce/frames)
|
17
18
|
|
@@ -110,6 +111,9 @@ accounts = client.query("select Id, Something__c from Account where Id = 'someid
|
|
110
111
|
account = records.first
|
111
112
|
# => #<Restforce::SObject >
|
112
113
|
|
114
|
+
account.sobject_type
|
115
|
+
# => 'Account'
|
116
|
+
|
113
117
|
account.Id
|
114
118
|
# => "someid"
|
115
119
|
|
@@ -204,6 +208,25 @@ _See also: http://www.salesforce.com/us/developer/docs/api_rest/Content/dome_del
|
|
204
208
|
|
205
209
|
* * *
|
206
210
|
|
211
|
+
### describe(sobject)
|
212
|
+
|
213
|
+
If no parameter is given, it will return the global describe. If the name of an
|
214
|
+
sobject is given, it will return the describe for that sobject.
|
215
|
+
|
216
|
+
```ruby
|
217
|
+
# get the global describe for all sobjects
|
218
|
+
client.describe
|
219
|
+
# => { ... }
|
220
|
+
|
221
|
+
# get the describe for the Account object
|
222
|
+
client.describe('Account')
|
223
|
+
# => { ... }
|
224
|
+
```
|
225
|
+
|
226
|
+
_See also: http://www.salesforce.com/us/developer/docs/api_rest/Content/dome_describeGlobal.htm, http://www.salesforce.com/us/developer/docs/api_rest/Content/dome_sobject_describe.htm_
|
227
|
+
|
228
|
+
* * *
|
229
|
+
|
207
230
|
### File Uploads
|
208
231
|
|
209
232
|
Using the new [Blob Data](http://www.salesforce.com/us/developer/docs/api_rest/Content/dome_sobject_insert_update_blob.htm) api feature (500mb limit):
|
@@ -269,6 +292,15 @@ Restforce.configure do |config|
|
|
269
292
|
end
|
270
293
|
```
|
271
294
|
|
295
|
+
If you enable caching, you can disable caching on a per-request basis by using
|
296
|
+
.without_caching:
|
297
|
+
|
298
|
+
```ruby
|
299
|
+
client.without_caching do
|
300
|
+
client.query('select Id from Account')
|
301
|
+
end
|
302
|
+
```
|
303
|
+
|
272
304
|
* * *
|
273
305
|
|
274
306
|
### Logging/Debugging
|
@@ -279,25 +311,29 @@ You can easily inspect what Restforce is sending/receiving by setting
|
|
279
311
|
```ruby
|
280
312
|
Restforce.log = true
|
281
313
|
client = Restforce.new.query('select Id, Name from Account')
|
314
|
+
```
|
282
315
|
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
#
|
288
|
-
#
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
#
|
297
|
-
#
|
298
|
-
|
299
|
-
|
300
|
-
|
316
|
+
**Log Output**
|
317
|
+
|
318
|
+
```
|
319
|
+
I, [2012-09-11T21:54:00.488991 #24032] INFO -- : post https://login.salesforce.com/services/oauth2/token
|
320
|
+
D, [2012-09-11T21:54:00.489078 #24032] DEBUG -- request:
|
321
|
+
I, [2012-09-11T21:54:00.997295 #24032] INFO -- Status: 200
|
322
|
+
D, [2012-09-11T21:54:00.997391 #24032] DEBUG -- response headers: server: ""
|
323
|
+
content-type: "application/json; charset=UTF-8"
|
324
|
+
transfer-encoding: "chunked"
|
325
|
+
date: "Wed, 12 Sep 2012 04:53:59 GMT"
|
326
|
+
connection: "close"
|
327
|
+
D, [2012-09-11T21:54:00.997431 #24032] DEBUG -- response body: { ... }
|
328
|
+
I, [2012-09-11T21:54:00.998985 #24032] INFO -- : get https://na9.salesforce.com/services/data/v24.0/query?q=select+Id%2C+Name+from+Account
|
329
|
+
D, [2012-09-11T21:54:00.999040 #24032] DEBUG -- request: Authorization: "OAuth token"
|
330
|
+
I, [2012-09-11T21:54:01.622874 #24032] INFO -- Status: 200
|
331
|
+
D, [2012-09-11T21:54:01.623001 #24032] DEBUG -- response headers: server: ""
|
332
|
+
content-type: "application/json; charset=UTF-8"
|
333
|
+
transfer-encoding: "chunked"
|
334
|
+
date: "Wed, 12 Sep 2012 04:54:00 GMT"
|
335
|
+
connection: "close"
|
336
|
+
D, [2012-09-11T21:54:01.623058 #24032] DEBUG -- response body: { ... }
|
301
337
|
```
|
302
338
|
|
303
339
|
## Contributing
|
data/lib/restforce.rb
CHANGED
data/lib/restforce/client.rb
CHANGED
@@ -52,21 +52,13 @@ module Restforce
|
|
52
52
|
raise 'Please specify a hash of options' unless options.is_a?(Hash)
|
53
53
|
@options = {}.tap do |options|
|
54
54
|
[:username, :password, :security_token, :client_id, :client_secret, :host,
|
55
|
-
:api_version, :oauth_token, :refresh_token, :instance_url, :cache].each do |option|
|
55
|
+
:api_version, :oauth_token, :refresh_token, :instance_url, :cache, :authentication_retries].each do |option|
|
56
56
|
options[option] = Restforce.configuration.send option
|
57
57
|
end
|
58
58
|
end
|
59
59
|
@options.merge!(options)
|
60
60
|
end
|
61
61
|
|
62
|
-
# Public: Get the global describe for all sobjects.
|
63
|
-
#
|
64
|
-
# Returns the Hash representation of the describe call.
|
65
|
-
def describe_sobjects
|
66
|
-
response = api_get 'sobjects'
|
67
|
-
response.body['sobjects']
|
68
|
-
end
|
69
|
-
|
70
62
|
# Public: Get the names of all sobjects on the org.
|
71
63
|
#
|
72
64
|
# Examples
|
@@ -77,7 +69,7 @@ module Restforce
|
|
77
69
|
#
|
78
70
|
# Returns an Array of String names for each SObject.
|
79
71
|
def list_sobjects
|
80
|
-
|
72
|
+
describe.collect { |sobject| sobject['name'] }
|
81
73
|
end
|
82
74
|
|
83
75
|
# Public: Returns a detailed describe result for the specified sobject
|
@@ -86,14 +78,23 @@ module Restforce
|
|
86
78
|
#
|
87
79
|
# Examples
|
88
80
|
#
|
81
|
+
# # get the global describe for all sobjects
|
82
|
+
# client.describe
|
83
|
+
# # => { ... }
|
84
|
+
#
|
89
85
|
# # get the describe for the Account object
|
90
86
|
# client.describe('Account')
|
91
87
|
# # => { ... }
|
92
88
|
#
|
93
89
|
# Returns the Hash representation of the describe call.
|
94
|
-
def describe(sobject)
|
95
|
-
|
96
|
-
|
90
|
+
def describe(sobject=nil)
|
91
|
+
if sobject
|
92
|
+
response = api_get "sobjects/#{sobject.to_s}/describe"
|
93
|
+
response.body
|
94
|
+
else
|
95
|
+
response = api_get 'sobjects'
|
96
|
+
response.body['sobjects']
|
97
|
+
end
|
97
98
|
end
|
98
99
|
|
99
100
|
# Public: Get the current organization's Id.
|
@@ -249,6 +250,18 @@ module Restforce
|
|
249
250
|
true
|
250
251
|
end
|
251
252
|
|
253
|
+
# Public: Runs the block with caching disabled.
|
254
|
+
#
|
255
|
+
# block - A query/describe/etc.
|
256
|
+
#
|
257
|
+
# Returns the result of the block
|
258
|
+
def without_caching(&block)
|
259
|
+
@options[:perform_caching] = false
|
260
|
+
block.call
|
261
|
+
ensure
|
262
|
+
@options.delete(:perform_caching)
|
263
|
+
end
|
264
|
+
|
252
265
|
# Public: Subscribe to a PushTopic
|
253
266
|
#
|
254
267
|
# channel - The name of the PushTopic channel to subscribe to.
|
@@ -267,6 +280,19 @@ module Restforce
|
|
267
280
|
connection.headers.delete('X-ForceAuthenticate')
|
268
281
|
end
|
269
282
|
|
283
|
+
# Public: Decodes a signed request received from Force.com Canvas.
|
284
|
+
#
|
285
|
+
# message - The POST message containing the signed request from Salesforce.
|
286
|
+
#
|
287
|
+
# Returns the Hash context if the message is valid.
|
288
|
+
def decode_signed_request(message)
|
289
|
+
raise 'client_secret not set' unless @options[:client_secret]
|
290
|
+
encryped_secret, payload = message.split('.')
|
291
|
+
digest = OpenSSL::Digest::Digest.new('sha256')
|
292
|
+
signature = Base64.encode64(OpenSSL::HMAC.hexdigest(digest, @options[:client_secret], payload))
|
293
|
+
JSON.parse(Base64.decode64(payload)) if encryped_secret == signature
|
294
|
+
end
|
295
|
+
|
270
296
|
# Public: Helper methods for performing arbitrary actions against the API using
|
271
297
|
# various HTTP verbs.
|
272
298
|
#
|
@@ -328,7 +354,7 @@ module Restforce
|
|
328
354
|
builder.use Restforce::Middleware::InstanceURL, self, @options
|
329
355
|
builder.use Restforce::Middleware::RaiseError
|
330
356
|
builder.response :json
|
331
|
-
builder.use Restforce::Middleware::Caching, cache if cache
|
357
|
+
builder.use Restforce::Middleware::Caching, cache, @options if cache
|
332
358
|
builder.use Restforce::Middleware::Logger, Restforce.configuration.logger if Restforce.log?
|
333
359
|
builder.adapter Faraday.default_adapter
|
334
360
|
end
|
data/lib/restforce/config.rb
CHANGED
@@ -60,9 +60,13 @@ module Restforce
|
|
60
60
|
# requests will be cached.
|
61
61
|
attr_accessor :cache
|
62
62
|
|
63
|
+
# The number of times reauthentication should be tried before failing.
|
64
|
+
attr_accessor :authentication_retries
|
65
|
+
|
63
66
|
def initialize
|
64
|
-
@api_version
|
65
|
-
@host
|
67
|
+
@api_version ||= '24.0'
|
68
|
+
@host ||= 'login.salesforce.com'
|
69
|
+
@authentication_retries ||= 3
|
66
70
|
end
|
67
71
|
|
68
72
|
def logger
|
@@ -7,16 +7,21 @@ module Restforce
|
|
7
7
|
class Middleware::Authentication < Restforce::Middleware
|
8
8
|
|
9
9
|
def call(env)
|
10
|
+
retries = @options[:authentication_retries]
|
10
11
|
request_body = env[:body]
|
11
12
|
request = env[:request]
|
12
13
|
begin
|
13
14
|
return authenticate! if force_authenticate?(env)
|
14
15
|
@app.call(env)
|
15
16
|
rescue Restforce::UnauthorizedError
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
17
|
+
if retries > 0
|
18
|
+
authenticate!
|
19
|
+
env[:body] = request_body
|
20
|
+
env[:request] = request
|
21
|
+
retries -= 1
|
22
|
+
retry
|
23
|
+
end
|
24
|
+
raise
|
20
25
|
end
|
21
26
|
end
|
22
27
|
|
@@ -1,11 +1,19 @@
|
|
1
1
|
module Restforce
|
2
2
|
class Middleware::Caching < FaradayMiddleware::Caching
|
3
3
|
|
4
|
+
def call(env)
|
5
|
+
perform_caching? ? super : @app.call(env)
|
6
|
+
end
|
7
|
+
|
4
8
|
# We don't want to cache requests for different clients, so append the
|
5
9
|
# oauth token to the cache key.
|
6
10
|
def cache_key(env)
|
7
11
|
super(env) + env[:request_headers][Restforce::Middleware::Authorization::AUTH_HEADER].gsub(/\s/, '')
|
8
12
|
end
|
9
13
|
|
14
|
+
def perform_caching?
|
15
|
+
!@options.has_key?(:perform_caching) || @options[:perform_caching]
|
16
|
+
end
|
17
|
+
|
10
18
|
end
|
11
19
|
end
|
data/lib/restforce/version.rb
CHANGED
data/spec/lib/client_spec.rb
CHANGED
@@ -52,19 +52,6 @@ shared_examples_for 'methods' do
|
|
52
52
|
it { should eq Restforce::Middleware::Authentication::Token }
|
53
53
|
end
|
54
54
|
end
|
55
|
-
|
56
|
-
describe '.describe_sobjects' do
|
57
|
-
before do
|
58
|
-
@request = stub_api_request :sobjects, with: 'sobject/describe_sobjects_success_response'
|
59
|
-
end
|
60
|
-
|
61
|
-
after do
|
62
|
-
@request.should have_been_requested
|
63
|
-
end
|
64
|
-
|
65
|
-
subject { client.describe_sobjects }
|
66
|
-
it { should be_an Array }
|
67
|
-
end
|
68
55
|
|
69
56
|
describe '.list_sobjects' do
|
70
57
|
before do
|
@@ -81,16 +68,31 @@ shared_examples_for 'methods' do
|
|
81
68
|
end
|
82
69
|
|
83
70
|
describe '.describe' do
|
84
|
-
|
85
|
-
|
86
|
-
|
71
|
+
context 'with no arguments' do
|
72
|
+
before do
|
73
|
+
@request = stub_api_request :sobjects, with: 'sobject/describe_sobjects_success_response'
|
74
|
+
end
|
87
75
|
|
88
|
-
|
89
|
-
|
76
|
+
after do
|
77
|
+
@request.should have_been_requested
|
78
|
+
end
|
79
|
+
|
80
|
+
subject { client.describe }
|
81
|
+
it { should be_an Array }
|
90
82
|
end
|
91
83
|
|
92
|
-
|
93
|
-
|
84
|
+
context 'with an argument' do
|
85
|
+
before do
|
86
|
+
@request = stub_api_request 'sobjects/Whizbang/describe', with: 'sobject/sobject_describe_success_response'
|
87
|
+
end
|
88
|
+
|
89
|
+
after do
|
90
|
+
@request.should have_been_requested
|
91
|
+
end
|
92
|
+
|
93
|
+
subject { client.describe('Whizbang') }
|
94
|
+
its(['name']) { should eq 'Whizbang' }
|
95
|
+
end
|
94
96
|
end
|
95
97
|
|
96
98
|
describe '.query' do
|
@@ -333,6 +335,42 @@ shared_examples_for 'methods' do
|
|
333
335
|
subject { client.send :cache }
|
334
336
|
it { should eq cache }
|
335
337
|
end
|
338
|
+
|
339
|
+
describe '.decode_signed_request' do
|
340
|
+
subject { client.decode_signed_request(message) }
|
341
|
+
|
342
|
+
context 'when the message is valid' do
|
343
|
+
let(:data) { Base64.encode64('{ "key": "value" }') }
|
344
|
+
let(:message) do
|
345
|
+
digest = OpenSSL::Digest::Digest.new('sha256')
|
346
|
+
signature = Base64.encode64(OpenSSL::HMAC.hexdigest(digest, client_secret, data))
|
347
|
+
"#{signature}.#{data}"
|
348
|
+
end
|
349
|
+
|
350
|
+
it { should eq('key' => 'value') }
|
351
|
+
end
|
352
|
+
|
353
|
+
context 'when the message is invalid' do
|
354
|
+
let(:message) { 'foobar.awdkjkj' }
|
355
|
+
it { should be_nil }
|
356
|
+
end
|
357
|
+
end
|
358
|
+
|
359
|
+
describe '.without_caching' do
|
360
|
+
let(:cache) { double('cache') }
|
361
|
+
|
362
|
+
before do
|
363
|
+
@request = stub_api_request 'query\?q=SELECT%20some,%20fields%20FROM%20object', with: 'sobject/query_success_response'
|
364
|
+
cache.should_receive(:fetch).never
|
365
|
+
end
|
366
|
+
|
367
|
+
after do
|
368
|
+
@request.should have_been_requested
|
369
|
+
end
|
370
|
+
|
371
|
+
subject { client.without_caching { client.query('SELECT some, fields FROM object') } }
|
372
|
+
it { should be_an Array }
|
373
|
+
end
|
336
374
|
end
|
337
375
|
|
338
376
|
describe 'with mashify middleware' do
|
data/spec/lib/config_spec.rb
CHANGED
@@ -11,8 +11,9 @@ describe Restforce do
|
|
11
11
|
it { should be_a Restforce::Configuration }
|
12
12
|
|
13
13
|
context 'by default' do
|
14
|
-
its(:api_version)
|
15
|
-
its(:host)
|
14
|
+
its(:api_version) { should eq '24.0' }
|
15
|
+
its(:host) { should eq 'login.salesforce.com' }
|
16
|
+
its(:authentication_retries) { should eq 3 }
|
16
17
|
[:username, :password, :security_token, :client_id, :client_secret,
|
17
18
|
:oauth_token, :refresh_token, :instance_url].each do |attr|
|
18
19
|
its(attr) { should be_nil }
|
@@ -3,7 +3,8 @@ require 'spec_helper'
|
|
3
3
|
describe Restforce::Middleware::Authentication do
|
4
4
|
let(:app) { double('app') }
|
5
5
|
let(:env) { { } }
|
6
|
-
let(:
|
6
|
+
let(:retries) { 3 }
|
7
|
+
let(:options) { { host: 'login.salesforce.com', authentication_retries: retries } }
|
7
8
|
let(:middleware) { described_class.new app, nil, options }
|
8
9
|
|
9
10
|
describe '.authenticate!' do
|
@@ -38,6 +39,18 @@ describe Restforce::Middleware::Authentication do
|
|
38
39
|
middleware.call(env)
|
39
40
|
end
|
40
41
|
end
|
42
|
+
|
43
|
+
context 'when the retry limit is reached' do
|
44
|
+
before do
|
45
|
+
app.should_receive(:call).twice.and_raise(Restforce::UnauthorizedError)
|
46
|
+
middleware.should_receive(:authenticate!)
|
47
|
+
end
|
48
|
+
|
49
|
+
let(:retries) { 1 }
|
50
|
+
it 'should raise an exception' do
|
51
|
+
expect { middleware.call(env) }.to raise_error Restforce::UnauthorizedError
|
52
|
+
end
|
53
|
+
end
|
41
54
|
end
|
42
55
|
|
43
56
|
describe '.connection' do
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: restforce
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-09-
|
12
|
+
date: 2012-09-21 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rake
|