restforce 0.1.8 → 0.1.9
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 +6 -5
- data/lib/restforce/client.rb +14 -373
- data/lib/restforce/client/api.rb +257 -0
- data/lib/restforce/client/authentication.rb +40 -0
- data/lib/restforce/client/caching.rb +26 -0
- data/lib/restforce/client/canvas.rb +17 -0
- data/lib/restforce/client/connection.rb +47 -0
- data/lib/restforce/client/streaming.rb +31 -0
- data/lib/restforce/sobject.rb +10 -0
- data/lib/restforce/version.rb +1 -1
- data/spec/lib/client_spec.rb +0 -6
- data/spec/lib/sobject_spec.rb +46 -2
- metadata +14 -2
data/README.md
CHANGED
@@ -15,7 +15,7 @@ It attempts to solve a couple of key issues that the databasedotcom gem has been
|
|
15
15
|
* A clean and modular architecture using [Faraday middleware](https://github.com/technoweenie/faraday)
|
16
16
|
* Support for decoding [Force.com Canvas](http://www.salesforce.com/us/developer/docs/platform_connectpre/canvas_framework.pdf) signed requests. (NEW!)
|
17
17
|
|
18
|
-
[Documentation](http://rubydoc.info/gems/restforce/frames)
|
18
|
+
[Documentation](http://rubydoc.info/gems/restforce/frames) | [Changelog](http://revision.io/restforce)
|
19
19
|
|
20
20
|
## Installation
|
21
21
|
|
@@ -98,7 +98,7 @@ Restforce.configure do |config|
|
|
98
98
|
end
|
99
99
|
```
|
100
100
|
|
101
|
-
### Bang methods
|
101
|
+
### Bang! methods
|
102
102
|
|
103
103
|
All the CRUD methods (create, update, upsert, destroy) have equivalent methods with
|
104
104
|
a ! at the end (create!, update!, upsert!, destroy!), which can be used if you need
|
@@ -294,9 +294,6 @@ require 'faye'
|
|
294
294
|
# Initialize a client with your username/password/oauth token/etc.
|
295
295
|
client = Restforce.new
|
296
296
|
|
297
|
-
# Force an authentication request.
|
298
|
-
client.authenticate!
|
299
|
-
|
300
297
|
# Create a PushTopic for subscribing to Account changes.
|
301
298
|
client.create! 'PushTopic', {
|
302
299
|
ApiVersion: '23.0',
|
@@ -372,6 +369,10 @@ ActiveSupport::Notifications.subscribe('request.faraday') do |name, start, finis
|
|
372
369
|
end
|
373
370
|
```
|
374
371
|
|
372
|
+
## Force.com Canvas
|
373
|
+
|
374
|
+
You can use Restforce to decode signed requests from Salesforce. See [the example app](https://gist.github.com/4052312).
|
375
|
+
|
375
376
|
## Contributing
|
376
377
|
|
377
378
|
1. Fork it
|
data/lib/restforce/client.rb
CHANGED
@@ -1,5 +1,19 @@
|
|
1
|
+
require 'restforce/client/connection'
|
2
|
+
require 'restforce/client/authentication'
|
3
|
+
require 'restforce/client/streaming'
|
4
|
+
require 'restforce/client/caching'
|
5
|
+
require 'restforce/client/canvas'
|
6
|
+
require 'restforce/client/api'
|
7
|
+
|
1
8
|
module Restforce
|
2
9
|
class Client
|
10
|
+
include Restforce::Client::Connection
|
11
|
+
include Restforce::Client::Authentication
|
12
|
+
include Restforce::Client::Streaming
|
13
|
+
include Restforce::Client::Caching
|
14
|
+
include Restforce::Client::Canvas
|
15
|
+
include Restforce::Client::API
|
16
|
+
|
3
17
|
OPTIONS = [:username, :password, :security_token, :client_id, :client_secret, :host, :compress,
|
4
18
|
:api_version, :oauth_token, :refresh_token, :instance_url, :cache, :authentication_retries]
|
5
19
|
|
@@ -63,378 +77,5 @@ module Restforce
|
|
63
77
|
@options = Hash[OPTIONS.map { |option| [option, Restforce.configuration.send(option)] }]
|
64
78
|
@options.merge! opts
|
65
79
|
end
|
66
|
-
|
67
|
-
# Public: Get the names of all sobjects on the org.
|
68
|
-
#
|
69
|
-
# Examples
|
70
|
-
#
|
71
|
-
# # get the names of all sobjects on the org
|
72
|
-
# client.list_sobjects
|
73
|
-
# # => ['Account', 'Lead', ... ]
|
74
|
-
#
|
75
|
-
# Returns an Array of String names for each SObject.
|
76
|
-
def list_sobjects
|
77
|
-
describe.collect { |sobject| sobject['name'] }
|
78
|
-
end
|
79
|
-
|
80
|
-
# Public: Returns a detailed describe result for the specified sobject
|
81
|
-
#
|
82
|
-
# sobject - Stringish name of the sobject (default: nil).
|
83
|
-
#
|
84
|
-
# Examples
|
85
|
-
#
|
86
|
-
# # get the global describe for all sobjects
|
87
|
-
# client.describe
|
88
|
-
# # => { ... }
|
89
|
-
#
|
90
|
-
# # get the describe for the Account object
|
91
|
-
# client.describe('Account')
|
92
|
-
# # => { ... }
|
93
|
-
#
|
94
|
-
# Returns the Hash representation of the describe call.
|
95
|
-
def describe(sobject=nil)
|
96
|
-
if sobject
|
97
|
-
api_get("sobjects/#{sobject.to_s}/describe").body
|
98
|
-
else
|
99
|
-
api_get('sobjects').body['sobjects']
|
100
|
-
end
|
101
|
-
end
|
102
|
-
|
103
|
-
# Public: Get the current organization's Id.
|
104
|
-
#
|
105
|
-
# Examples
|
106
|
-
#
|
107
|
-
# client.org_id
|
108
|
-
# # => '00Dx0000000BV7z'
|
109
|
-
#
|
110
|
-
# Returns the String organization Id
|
111
|
-
def org_id
|
112
|
-
query('select id from Organization').first['Id']
|
113
|
-
end
|
114
|
-
|
115
|
-
# Public: Executs a SOQL query and returns the result.
|
116
|
-
#
|
117
|
-
# soql - A SOQL expression.
|
118
|
-
#
|
119
|
-
# Examples
|
120
|
-
#
|
121
|
-
# # Find the names of all Accounts
|
122
|
-
# client.query('select Name from Account').map(&:Name)
|
123
|
-
# # => ['Foo Bar Inc.', 'Whizbang Corp']
|
124
|
-
#
|
125
|
-
# Returns a Restforce::Collection if Restforce.configuration.mashify is true.
|
126
|
-
# Returns an Array of Hash for each record in the result if Restforce.configuration.mashify is false.
|
127
|
-
def query(soql)
|
128
|
-
response = api_get 'query', :q => soql
|
129
|
-
mashify? ? response.body : response.body['records']
|
130
|
-
end
|
131
|
-
|
132
|
-
# Public: Perform a SOSL search
|
133
|
-
#
|
134
|
-
# sosl - A SOSL expression.
|
135
|
-
#
|
136
|
-
# Examples
|
137
|
-
#
|
138
|
-
# # Find all occurrences of 'bar'
|
139
|
-
# client.search('FIND {bar}')
|
140
|
-
# # => #<Restforce::Collection >
|
141
|
-
#
|
142
|
-
# # Find accounts match the term 'genepoint' and return the Name field
|
143
|
-
# client.search('FIND {genepoint} RETURNING Account (Name)').map(&:Name)
|
144
|
-
# # => ['GenePoint']
|
145
|
-
#
|
146
|
-
# Returns a Restforce::Collection if Restforce.configuration.mashify is true.
|
147
|
-
# Returns an Array of Hash for each record in the result if Restforce.configuration.mashify is false.
|
148
|
-
def search(sosl)
|
149
|
-
api_get('search', :q => sosl).body
|
150
|
-
end
|
151
|
-
|
152
|
-
# Public: Insert a new record.
|
153
|
-
#
|
154
|
-
# Examples
|
155
|
-
#
|
156
|
-
# # Add a new account
|
157
|
-
# client.create('Account', Name: 'Foobar Inc.')
|
158
|
-
# # => '0016000000MRatd'
|
159
|
-
#
|
160
|
-
# Returns the String Id of the newly created sobject. Returns false if
|
161
|
-
# something bad happens
|
162
|
-
def create(sobject, attrs)
|
163
|
-
create!(sobject, attrs)
|
164
|
-
rescue *exceptions
|
165
|
-
false
|
166
|
-
end
|
167
|
-
alias_method :insert, :create
|
168
|
-
|
169
|
-
# See .create
|
170
|
-
#
|
171
|
-
# Returns the String Id of the newly created sobject. Raises an error if
|
172
|
-
# something bad happens.
|
173
|
-
def create!(sobject, attrs)
|
174
|
-
api_post("sobjects/#{sobject}", attrs).body['id']
|
175
|
-
end
|
176
|
-
alias_method :insert!, :create!
|
177
|
-
|
178
|
-
# Public: Update a record.
|
179
|
-
#
|
180
|
-
# Examples
|
181
|
-
#
|
182
|
-
# # Update the Account with Id '0016000000MRatd'
|
183
|
-
# client.update('Account', Id: '0016000000MRatd', Name: 'Whizbang Corp')
|
184
|
-
#
|
185
|
-
# Returns true if the sobject was successfully updated, false otherwise.
|
186
|
-
def update(sobject, attrs)
|
187
|
-
update!(sobject, attrs)
|
188
|
-
rescue *exceptions
|
189
|
-
false
|
190
|
-
end
|
191
|
-
|
192
|
-
# See .update
|
193
|
-
#
|
194
|
-
# Returns true if the sobject was successfully updated, raises an error
|
195
|
-
# otherwise.
|
196
|
-
def update!(sobject, attrs)
|
197
|
-
id = attrs.has_key?(:Id) ? attrs.delete(:Id) : attrs.delete('Id')
|
198
|
-
raise 'Id field missing.' unless id
|
199
|
-
api_patch "sobjects/#{sobject}/#{id}", attrs
|
200
|
-
true
|
201
|
-
end
|
202
|
-
|
203
|
-
# Public: Update or Create a record based on an external ID
|
204
|
-
#
|
205
|
-
# sobject - The name of the sobject to created.
|
206
|
-
# field - The name of the external Id field to match against.
|
207
|
-
# attrs - Hash of attributes for the record.
|
208
|
-
#
|
209
|
-
# Examples
|
210
|
-
#
|
211
|
-
# # Update the record with external ID of 12
|
212
|
-
# client.upsert('Account', 'External__c', External__c: 12, Name: 'Foobar')
|
213
|
-
#
|
214
|
-
# Returns true if the record was found and updated.
|
215
|
-
# Returns the Id of the newly created record if the record was created.
|
216
|
-
# Returns false if something bad happens.
|
217
|
-
def upsert(sobject, field, attrs)
|
218
|
-
upsert!(sobject, field, attrs)
|
219
|
-
rescue *exceptions
|
220
|
-
false
|
221
|
-
end
|
222
|
-
|
223
|
-
# See .upsert
|
224
|
-
#
|
225
|
-
# Returns true if the record was found and updated.
|
226
|
-
# Returns the Id of the newly created record if the record was created.
|
227
|
-
# Raises an error if something bad happens.
|
228
|
-
def upsert!(sobject, field, attrs)
|
229
|
-
external_id = attrs.has_key?(field.to_sym) ? attrs.delete(field.to_sym) : attrs.delete(field.to_s)
|
230
|
-
response = api_patch "sobjects/#{sobject}/#{field.to_s}/#{external_id}", attrs
|
231
|
-
(response.body && response.body['id']) ? response.body['id'] : true
|
232
|
-
end
|
233
|
-
|
234
|
-
# Public: Delete a record.
|
235
|
-
#
|
236
|
-
# Examples
|
237
|
-
#
|
238
|
-
# # Delete the Account with Id '0016000000MRatd'
|
239
|
-
# client.delete('Account', '0016000000MRatd')
|
240
|
-
#
|
241
|
-
# Returns true if the sobject was successfully deleted, false otherwise.
|
242
|
-
def destroy(sobject, id)
|
243
|
-
destroy!(sobject, id)
|
244
|
-
rescue *exceptions
|
245
|
-
false
|
246
|
-
end
|
247
|
-
|
248
|
-
# See .destroy
|
249
|
-
#
|
250
|
-
# Returns true of the sobject was successfully deleted, raises an error
|
251
|
-
# otherwise.
|
252
|
-
def destroy!(sobject, id)
|
253
|
-
api_delete "sobjects/#{sobject}/#{id}"
|
254
|
-
true
|
255
|
-
end
|
256
|
-
|
257
|
-
# Public: Runs the block with caching disabled.
|
258
|
-
#
|
259
|
-
# block - A query/describe/etc.
|
260
|
-
#
|
261
|
-
# Returns the result of the block
|
262
|
-
def without_caching(&block)
|
263
|
-
@options[:perform_caching] = false
|
264
|
-
block.call
|
265
|
-
ensure
|
266
|
-
@options.delete(:perform_caching)
|
267
|
-
end
|
268
|
-
|
269
|
-
# Public: Subscribe to a PushTopic
|
270
|
-
#
|
271
|
-
# channel - The name of the PushTopic channel to subscribe to.
|
272
|
-
# block - A block to run when a new message is received.
|
273
|
-
#
|
274
|
-
# Returns a Faye::Subscription
|
275
|
-
def subscribe(channel, &block)
|
276
|
-
faye.subscribe "/topic/#{channel}", &block
|
277
|
-
end
|
278
|
-
|
279
|
-
# Public: Force an authentication
|
280
|
-
def authenticate!
|
281
|
-
raise 'No authentication middleware present' unless authentication_middleware
|
282
|
-
middleware = authentication_middleware.new nil, self, @options
|
283
|
-
middleware.authenticate!
|
284
|
-
end
|
285
|
-
|
286
|
-
# Public: Decodes a signed request received from Force.com Canvas.
|
287
|
-
#
|
288
|
-
# message - The POST message containing the signed request from Salesforce.
|
289
|
-
#
|
290
|
-
# Returns the Hash context if the message is valid.
|
291
|
-
def decode_signed_request(message)
|
292
|
-
raise 'client_secret not set' unless @options[:client_secret]
|
293
|
-
Restforce.decode_signed_request(message, @options[:client_secret])
|
294
|
-
end
|
295
|
-
|
296
|
-
# Public: Helper methods for performing arbitrary actions against the API using
|
297
|
-
# various HTTP verbs.
|
298
|
-
#
|
299
|
-
# Examples
|
300
|
-
#
|
301
|
-
# # Perform a get request
|
302
|
-
# client.get '/services/data/v24.0/sobjects'
|
303
|
-
# client.api_get 'sobjects'
|
304
|
-
#
|
305
|
-
# # Perform a post request
|
306
|
-
# client.post '/services/data/v24.0/sobjects/Account', { ... }
|
307
|
-
# client.api_post 'sobjects/Account', { ... }
|
308
|
-
#
|
309
|
-
# # Perform a put request
|
310
|
-
# client.put '/services/data/v24.0/sobjects/Account/001D000000INjVe', { ... }
|
311
|
-
# client.api_put 'sobjects/Account/001D000000INjVe', { ... }
|
312
|
-
#
|
313
|
-
# # Perform a delete request
|
314
|
-
# client.delete '/services/data/v24.0/sobjects/Account/001D000000INjVe'
|
315
|
-
# client.api_delete 'sobjects/Account/001D000000INjVe'
|
316
|
-
#
|
317
|
-
# Returns the Faraday::Response.
|
318
|
-
[:get, :post, :put, :delete, :patch].each do |method|
|
319
|
-
define_method method do |*args|
|
320
|
-
retries = @options[:authentication_retries]
|
321
|
-
begin
|
322
|
-
connection.send(method, *args)
|
323
|
-
rescue Restforce::UnauthorizedError
|
324
|
-
if retries > 0
|
325
|
-
retries -= 1
|
326
|
-
connection.url_prefix = @options[:instance_url]
|
327
|
-
retry
|
328
|
-
end
|
329
|
-
raise
|
330
|
-
end
|
331
|
-
end
|
332
|
-
|
333
|
-
define_method :"api_#{method}" do |*args|
|
334
|
-
args[0] = api_path(args[0])
|
335
|
-
send(method, *args)
|
336
|
-
end
|
337
|
-
end
|
338
|
-
|
339
|
-
# Public: The Faraday::Builder instance used for the middleware stack. This
|
340
|
-
# can be used to insert an custom middleware.
|
341
|
-
#
|
342
|
-
# Examples
|
343
|
-
#
|
344
|
-
# # Add the instrumentation middleware for Rails.
|
345
|
-
# client.middleware.use FaradayMiddleware::Instrumentation
|
346
|
-
#
|
347
|
-
# Returns the Faraday::Builder for the Faraday connection.
|
348
|
-
def middleware
|
349
|
-
connection.builder
|
350
|
-
end
|
351
|
-
|
352
|
-
private
|
353
|
-
|
354
|
-
# Internal: Returns a path to an api endpoint
|
355
|
-
#
|
356
|
-
# Examples
|
357
|
-
#
|
358
|
-
# api_path('sobjects')
|
359
|
-
# # => '/services/data/v24.0/sobjects'
|
360
|
-
def api_path(path)
|
361
|
-
"/services/data/v#{@options[:api_version]}/#{path}"
|
362
|
-
end
|
363
|
-
|
364
|
-
# Internal: Internal faraday connection where all requests go through
|
365
|
-
def connection
|
366
|
-
@connection ||= Faraday.new(@options[:instance_url]) do |builder|
|
367
|
-
builder.use Restforce::Middleware::Mashify, self, @options
|
368
|
-
builder.use Restforce::Middleware::Multipart
|
369
|
-
builder.request :json
|
370
|
-
builder.use authentication_middleware, self, @options if authentication_middleware
|
371
|
-
builder.use Restforce::Middleware::Authorization, self, @options
|
372
|
-
builder.use Restforce::Middleware::InstanceURL, self, @options
|
373
|
-
builder.response :json
|
374
|
-
builder.use Restforce::Middleware::Caching, cache, @options if cache
|
375
|
-
builder.use FaradayMiddleware::FollowRedirects
|
376
|
-
builder.use Restforce::Middleware::RaiseError
|
377
|
-
builder.use Restforce::Middleware::Logger, Restforce.configuration.logger, @options if Restforce.log?
|
378
|
-
builder.use Restforce::Middleware::Gzip, self, @options
|
379
|
-
builder.adapter Faraday.default_adapter
|
380
|
-
end
|
381
|
-
end
|
382
|
-
|
383
|
-
# Internal: Determines what middleware will be used based on the options provided
|
384
|
-
def authentication_middleware
|
385
|
-
if username_password?
|
386
|
-
Restforce::Middleware::Authentication::Password
|
387
|
-
elsif oauth_refresh?
|
388
|
-
Restforce::Middleware::Authentication::Token
|
389
|
-
end
|
390
|
-
end
|
391
|
-
|
392
|
-
# Internal: Returns true if username/password (autonomous) flow should be used for
|
393
|
-
# authentication.
|
394
|
-
def username_password?
|
395
|
-
@options[:username] &&
|
396
|
-
@options[:password] &&
|
397
|
-
@options[:client_id] &&
|
398
|
-
@options[:client_secret]
|
399
|
-
end
|
400
|
-
|
401
|
-
# Internal: Returns true if oauth token refresh flow should be used for
|
402
|
-
# authentication.
|
403
|
-
def oauth_refresh?
|
404
|
-
@options[:refresh_token] &&
|
405
|
-
@options[:client_id] &&
|
406
|
-
@options[:client_secret]
|
407
|
-
end
|
408
|
-
|
409
|
-
# Internal: Cache to use for the caching middleware
|
410
|
-
def cache
|
411
|
-
@options[:cache]
|
412
|
-
end
|
413
|
-
|
414
|
-
# Internal: Returns true if the middlware stack includes the
|
415
|
-
# Restforce::Middleware::Mashify middleware.
|
416
|
-
def mashify?
|
417
|
-
middleware.handlers.index(Restforce::Middleware::Mashify)
|
418
|
-
end
|
419
|
-
|
420
|
-
# Internal: Errors that should be rescued from in non-bang methods
|
421
|
-
def exceptions
|
422
|
-
[Faraday::Error::ClientError]
|
423
|
-
end
|
424
|
-
|
425
|
-
# Internal: Faye client to use for subscribing to PushTopics
|
426
|
-
def faye
|
427
|
-
raise 'Instance URL missing. Call .authenticate! first.' unless @options[:instance_url]
|
428
|
-
@faye ||= Faye::Client.new("#{@options[:instance_url]}/cometd/#{@options[:api_version]}").tap do |client|
|
429
|
-
raise 'OAuth token missing. Call .authenticate! first.' unless @options[:oauth_token]
|
430
|
-
client.set_header 'Authorization', "OAuth #{@options[:oauth_token]}"
|
431
|
-
client.bind 'transport:down' do
|
432
|
-
Restforce.log "[COMETD DOWN]"
|
433
|
-
end
|
434
|
-
client.bind 'transport:up' do
|
435
|
-
Restforce.log "[COMETD UP]"
|
436
|
-
end
|
437
|
-
end
|
438
|
-
end
|
439
80
|
end
|
440
81
|
end
|
@@ -0,0 +1,257 @@
|
|
1
|
+
module Restforce
|
2
|
+
class Client
|
3
|
+
module API
|
4
|
+
|
5
|
+
# Public: Get the names of all sobjects on the org.
|
6
|
+
#
|
7
|
+
# Examples
|
8
|
+
#
|
9
|
+
# # get the names of all sobjects on the org
|
10
|
+
# client.list_sobjects
|
11
|
+
# # => ['Account', 'Lead', ... ]
|
12
|
+
#
|
13
|
+
# Returns an Array of String names for each SObject.
|
14
|
+
def list_sobjects
|
15
|
+
describe.collect { |sobject| sobject['name'] }
|
16
|
+
end
|
17
|
+
|
18
|
+
# Public: Returns a detailed describe result for the specified sobject
|
19
|
+
#
|
20
|
+
# sobject - Stringish name of the sobject (default: nil).
|
21
|
+
#
|
22
|
+
# Examples
|
23
|
+
#
|
24
|
+
# # get the global describe for all sobjects
|
25
|
+
# client.describe
|
26
|
+
# # => { ... }
|
27
|
+
#
|
28
|
+
# # get the describe for the Account object
|
29
|
+
# client.describe('Account')
|
30
|
+
# # => { ... }
|
31
|
+
#
|
32
|
+
# Returns the Hash representation of the describe call.
|
33
|
+
def describe(sobject=nil)
|
34
|
+
if sobject
|
35
|
+
api_get("sobjects/#{sobject.to_s}/describe").body
|
36
|
+
else
|
37
|
+
api_get('sobjects').body['sobjects']
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Public: Get the current organization's Id.
|
42
|
+
#
|
43
|
+
# Examples
|
44
|
+
#
|
45
|
+
# client.org_id
|
46
|
+
# # => '00Dx0000000BV7z'
|
47
|
+
#
|
48
|
+
# Returns the String organization Id
|
49
|
+
def org_id
|
50
|
+
query('select id from Organization').first['Id']
|
51
|
+
end
|
52
|
+
|
53
|
+
# Public: Executs a SOQL query and returns the result.
|
54
|
+
#
|
55
|
+
# soql - A SOQL expression.
|
56
|
+
#
|
57
|
+
# Examples
|
58
|
+
#
|
59
|
+
# # Find the names of all Accounts
|
60
|
+
# client.query('select Name from Account').map(&:Name)
|
61
|
+
# # => ['Foo Bar Inc.', 'Whizbang Corp']
|
62
|
+
#
|
63
|
+
# Returns a Restforce::Collection if Restforce.configuration.mashify is true.
|
64
|
+
# Returns an Array of Hash for each record in the result if Restforce.configuration.mashify is false.
|
65
|
+
def query(soql)
|
66
|
+
response = api_get 'query', :q => soql
|
67
|
+
mashify? ? response.body : response.body['records']
|
68
|
+
end
|
69
|
+
|
70
|
+
# Public: Perform a SOSL search
|
71
|
+
#
|
72
|
+
# sosl - A SOSL expression.
|
73
|
+
#
|
74
|
+
# Examples
|
75
|
+
#
|
76
|
+
# # Find all occurrences of 'bar'
|
77
|
+
# client.search('FIND {bar}')
|
78
|
+
# # => #<Restforce::Collection >
|
79
|
+
#
|
80
|
+
# # Find accounts match the term 'genepoint' and return the Name field
|
81
|
+
# client.search('FIND {genepoint} RETURNING Account (Name)').map(&:Name)
|
82
|
+
# # => ['GenePoint']
|
83
|
+
#
|
84
|
+
# Returns a Restforce::Collection if Restforce.configuration.mashify is true.
|
85
|
+
# Returns an Array of Hash for each record in the result if Restforce.configuration.mashify is false.
|
86
|
+
def search(sosl)
|
87
|
+
api_get('search', :q => sosl).body
|
88
|
+
end
|
89
|
+
|
90
|
+
# Public: Insert a new record.
|
91
|
+
#
|
92
|
+
# Examples
|
93
|
+
#
|
94
|
+
# # Add a new account
|
95
|
+
# client.create('Account', Name: 'Foobar Inc.')
|
96
|
+
# # => '0016000000MRatd'
|
97
|
+
#
|
98
|
+
# Returns the String Id of the newly created sobject. Returns false if
|
99
|
+
# something bad happens
|
100
|
+
def create(sobject, attrs)
|
101
|
+
create!(sobject, attrs)
|
102
|
+
rescue *exceptions
|
103
|
+
false
|
104
|
+
end
|
105
|
+
alias_method :insert, :create
|
106
|
+
|
107
|
+
# See .create
|
108
|
+
#
|
109
|
+
# Returns the String Id of the newly created sobject. Raises an error if
|
110
|
+
# something bad happens.
|
111
|
+
def create!(sobject, attrs)
|
112
|
+
api_post("sobjects/#{sobject}", attrs).body['id']
|
113
|
+
end
|
114
|
+
alias_method :insert!, :create!
|
115
|
+
|
116
|
+
# Public: Update a record.
|
117
|
+
#
|
118
|
+
# Examples
|
119
|
+
#
|
120
|
+
# # Update the Account with Id '0016000000MRatd'
|
121
|
+
# client.update('Account', Id: '0016000000MRatd', Name: 'Whizbang Corp')
|
122
|
+
#
|
123
|
+
# Returns true if the sobject was successfully updated, false otherwise.
|
124
|
+
def update(sobject, attrs)
|
125
|
+
update!(sobject, attrs)
|
126
|
+
rescue *exceptions
|
127
|
+
false
|
128
|
+
end
|
129
|
+
|
130
|
+
# See .update
|
131
|
+
#
|
132
|
+
# Returns true if the sobject was successfully updated, raises an error
|
133
|
+
# otherwise.
|
134
|
+
def update!(sobject, attrs)
|
135
|
+
id = attrs.has_key?(:Id) ? attrs.delete(:Id) : attrs.delete('Id')
|
136
|
+
raise 'Id field missing.' unless id
|
137
|
+
api_patch "sobjects/#{sobject}/#{id}", attrs
|
138
|
+
true
|
139
|
+
end
|
140
|
+
|
141
|
+
# Public: Update or Create a record based on an external ID
|
142
|
+
#
|
143
|
+
# sobject - The name of the sobject to created.
|
144
|
+
# field - The name of the external Id field to match against.
|
145
|
+
# attrs - Hash of attributes for the record.
|
146
|
+
#
|
147
|
+
# Examples
|
148
|
+
#
|
149
|
+
# # Update the record with external ID of 12
|
150
|
+
# client.upsert('Account', 'External__c', External__c: 12, Name: 'Foobar')
|
151
|
+
#
|
152
|
+
# Returns true if the record was found and updated.
|
153
|
+
# Returns the Id of the newly created record if the record was created.
|
154
|
+
# Returns false if something bad happens.
|
155
|
+
def upsert(sobject, field, attrs)
|
156
|
+
upsert!(sobject, field, attrs)
|
157
|
+
rescue *exceptions
|
158
|
+
false
|
159
|
+
end
|
160
|
+
|
161
|
+
# See .upsert
|
162
|
+
#
|
163
|
+
# Returns true if the record was found and updated.
|
164
|
+
# Returns the Id of the newly created record if the record was created.
|
165
|
+
# Raises an error if something bad happens.
|
166
|
+
def upsert!(sobject, field, attrs)
|
167
|
+
external_id = attrs.has_key?(field.to_sym) ? attrs.delete(field.to_sym) : attrs.delete(field.to_s)
|
168
|
+
response = api_patch "sobjects/#{sobject}/#{field.to_s}/#{external_id}", attrs
|
169
|
+
(response.body && response.body['id']) ? response.body['id'] : true
|
170
|
+
end
|
171
|
+
|
172
|
+
# Public: Delete a record.
|
173
|
+
#
|
174
|
+
# Examples
|
175
|
+
#
|
176
|
+
# # Delete the Account with Id '0016000000MRatd'
|
177
|
+
# client.delete('Account', '0016000000MRatd')
|
178
|
+
#
|
179
|
+
# Returns true if the sobject was successfully deleted, false otherwise.
|
180
|
+
def destroy(sobject, id)
|
181
|
+
destroy!(sobject, id)
|
182
|
+
rescue *exceptions
|
183
|
+
false
|
184
|
+
end
|
185
|
+
|
186
|
+
# See .destroy
|
187
|
+
#
|
188
|
+
# Returns true of the sobject was successfully deleted, raises an error
|
189
|
+
# otherwise.
|
190
|
+
def destroy!(sobject, id)
|
191
|
+
api_delete "sobjects/#{sobject}/#{id}"
|
192
|
+
true
|
193
|
+
end
|
194
|
+
|
195
|
+
# Public: Helper methods for performing arbitrary actions against the API using
|
196
|
+
# various HTTP verbs.
|
197
|
+
#
|
198
|
+
# Examples
|
199
|
+
#
|
200
|
+
# # Perform a get request
|
201
|
+
# client.get '/services/data/v24.0/sobjects'
|
202
|
+
# client.api_get 'sobjects'
|
203
|
+
#
|
204
|
+
# # Perform a post request
|
205
|
+
# client.post '/services/data/v24.0/sobjects/Account', { ... }
|
206
|
+
# client.api_post 'sobjects/Account', { ... }
|
207
|
+
#
|
208
|
+
# # Perform a put request
|
209
|
+
# client.put '/services/data/v24.0/sobjects/Account/001D000000INjVe', { ... }
|
210
|
+
# client.api_put 'sobjects/Account/001D000000INjVe', { ... }
|
211
|
+
#
|
212
|
+
# # Perform a delete request
|
213
|
+
# client.delete '/services/data/v24.0/sobjects/Account/001D000000INjVe'
|
214
|
+
# client.api_delete 'sobjects/Account/001D000000INjVe'
|
215
|
+
#
|
216
|
+
# Returns the Faraday::Response.
|
217
|
+
[:get, :post, :put, :delete, :patch].each do |method|
|
218
|
+
define_method method do |*args|
|
219
|
+
retries = @options[:authentication_retries]
|
220
|
+
begin
|
221
|
+
connection.send(method, *args)
|
222
|
+
rescue Restforce::UnauthorizedError
|
223
|
+
if retries > 0
|
224
|
+
retries -= 1
|
225
|
+
connection.url_prefix = @options[:instance_url]
|
226
|
+
retry
|
227
|
+
end
|
228
|
+
raise
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
define_method :"api_#{method}" do |*args|
|
233
|
+
args[0] = api_path(args[0])
|
234
|
+
send(method, *args)
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
private
|
239
|
+
|
240
|
+
# Internal: Returns a path to an api endpoint
|
241
|
+
#
|
242
|
+
# Examples
|
243
|
+
#
|
244
|
+
# api_path('sobjects')
|
245
|
+
# # => '/services/data/v24.0/sobjects'
|
246
|
+
def api_path(path)
|
247
|
+
"/services/data/v#{@options[:api_version]}/#{path}"
|
248
|
+
end
|
249
|
+
|
250
|
+
# Internal: Errors that should be rescued from in non-bang methods
|
251
|
+
def exceptions
|
252
|
+
[Faraday::Error::ClientError]
|
253
|
+
end
|
254
|
+
|
255
|
+
end
|
256
|
+
end
|
257
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Restforce
|
2
|
+
class Client
|
3
|
+
module Authentication
|
4
|
+
|
5
|
+
# Public: Force an authentication
|
6
|
+
def authenticate!
|
7
|
+
raise 'No authentication middleware present' unless authentication_middleware
|
8
|
+
middleware = authentication_middleware.new nil, self, @options
|
9
|
+
middleware.authenticate!
|
10
|
+
end
|
11
|
+
|
12
|
+
# Internal: Determines what middleware will be used based on the options provided
|
13
|
+
def authentication_middleware
|
14
|
+
if username_password?
|
15
|
+
Restforce::Middleware::Authentication::Password
|
16
|
+
elsif oauth_refresh?
|
17
|
+
Restforce::Middleware::Authentication::Token
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Internal: Returns true if username/password (autonomous) flow should be used for
|
22
|
+
# authentication.
|
23
|
+
def username_password?
|
24
|
+
@options[:username] &&
|
25
|
+
@options[:password] &&
|
26
|
+
@options[:client_id] &&
|
27
|
+
@options[:client_secret]
|
28
|
+
end
|
29
|
+
|
30
|
+
# Internal: Returns true if oauth token refresh flow should be used for
|
31
|
+
# authentication.
|
32
|
+
def oauth_refresh?
|
33
|
+
@options[:refresh_token] &&
|
34
|
+
@options[:client_id] &&
|
35
|
+
@options[:client_secret]
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Restforce
|
2
|
+
class Client
|
3
|
+
module Caching
|
4
|
+
|
5
|
+
# Public: Runs the block with caching disabled.
|
6
|
+
#
|
7
|
+
# block - A query/describe/etc.
|
8
|
+
#
|
9
|
+
# Returns the result of the block
|
10
|
+
def without_caching(&block)
|
11
|
+
@options[:perform_caching] = false
|
12
|
+
block.call
|
13
|
+
ensure
|
14
|
+
@options.delete(:perform_caching)
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
# Internal: Cache to use for the caching middleware
|
20
|
+
def cache
|
21
|
+
@options[:cache]
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Restforce
|
2
|
+
class Client
|
3
|
+
module Canvas
|
4
|
+
|
5
|
+
# Public: Decodes a signed request received from Force.com Canvas.
|
6
|
+
#
|
7
|
+
# message - The POST message containing the signed request from Salesforce.
|
8
|
+
#
|
9
|
+
# Returns the Hash context if the message is valid.
|
10
|
+
def decode_signed_request(message)
|
11
|
+
raise 'client_secret not set' unless @options[:client_secret]
|
12
|
+
Restforce.decode_signed_request(message, @options[:client_secret])
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Restforce
|
2
|
+
class Client
|
3
|
+
module Connection
|
4
|
+
|
5
|
+
# Public: The Faraday::Builder instance used for the middleware stack. This
|
6
|
+
# can be used to insert an custom middleware.
|
7
|
+
#
|
8
|
+
# Examples
|
9
|
+
#
|
10
|
+
# # Add the instrumentation middleware for Rails.
|
11
|
+
# client.middleware.use FaradayMiddleware::Instrumentation
|
12
|
+
#
|
13
|
+
# Returns the Faraday::Builder for the Faraday connection.
|
14
|
+
def middleware
|
15
|
+
connection.builder
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
# Internal: Internal faraday connection where all requests go through
|
21
|
+
def connection
|
22
|
+
@connection ||= Faraday.new(@options[:instance_url]) do |builder|
|
23
|
+
builder.use Restforce::Middleware::Mashify, self, @options
|
24
|
+
builder.use Restforce::Middleware::Multipart
|
25
|
+
builder.request :json
|
26
|
+
builder.use authentication_middleware, self, @options if authentication_middleware
|
27
|
+
builder.use Restforce::Middleware::Authorization, self, @options
|
28
|
+
builder.use Restforce::Middleware::InstanceURL, self, @options
|
29
|
+
builder.response :json
|
30
|
+
builder.use Restforce::Middleware::Caching, cache, @options if cache
|
31
|
+
builder.use FaradayMiddleware::FollowRedirects
|
32
|
+
builder.use Restforce::Middleware::RaiseError
|
33
|
+
builder.use Restforce::Middleware::Logger, Restforce.configuration.logger, @options if Restforce.log?
|
34
|
+
builder.use Restforce::Middleware::Gzip, self, @options
|
35
|
+
builder.adapter Faraday.default_adapter
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Internal: Returns true if the middlware stack includes the
|
40
|
+
# Restforce::Middleware::Mashify middleware.
|
41
|
+
def mashify?
|
42
|
+
middleware.handlers.index(Restforce::Middleware::Mashify)
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Restforce
|
2
|
+
class Client
|
3
|
+
module Streaming
|
4
|
+
|
5
|
+
# Public: Subscribe to a PushTopic
|
6
|
+
#
|
7
|
+
# channel - The name of the PushTopic channel to subscribe to.
|
8
|
+
# block - A block to run when a new message is received.
|
9
|
+
#
|
10
|
+
# Returns a Faye::Subscription
|
11
|
+
def subscribe(channel, &block)
|
12
|
+
faye.subscribe "/topic/#{channel}", &block
|
13
|
+
end
|
14
|
+
|
15
|
+
# Public: Faye client to use for subscribing to PushTopics
|
16
|
+
def faye
|
17
|
+
raise 'Instance URL missing. Call .authenticate! first.' unless @options[:instance_url]
|
18
|
+
@faye ||= Faye::Client.new("#{@options[:instance_url]}/cometd/#{@options[:api_version]}").tap do |client|
|
19
|
+
client.bind 'transport:down' do
|
20
|
+
Restforce.log "[COMETD DOWN]"
|
21
|
+
client.set_header 'Authorization', "OAuth #{authenticate!.access_token}"
|
22
|
+
end
|
23
|
+
client.bind 'transport:up' do
|
24
|
+
Restforce.log "[COMETD UP]"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
data/lib/restforce/sobject.rb
CHANGED
@@ -22,6 +22,11 @@ module Restforce
|
|
22
22
|
@client.update(sobject_type, attrs)
|
23
23
|
end
|
24
24
|
|
25
|
+
def save!
|
26
|
+
ensure_id
|
27
|
+
@client.update!(sobject_type, attrs)
|
28
|
+
end
|
29
|
+
|
25
30
|
# Public: Destroy this record.
|
26
31
|
#
|
27
32
|
# Examples
|
@@ -33,6 +38,11 @@ module Restforce
|
|
33
38
|
@client.destroy(sobject_type, self.Id)
|
34
39
|
end
|
35
40
|
|
41
|
+
def destroy!
|
42
|
+
ensure_id
|
43
|
+
@client.destroy!(sobject_type, self.Id)
|
44
|
+
end
|
45
|
+
|
36
46
|
# Public: Returns a hash representation of this object with the attributes
|
37
47
|
# key and parent/child relationships removed.
|
38
48
|
def attrs
|
data/lib/restforce/version.rb
CHANGED
data/spec/lib/client_spec.rb
CHANGED
@@ -424,12 +424,6 @@ shared_examples_for 'methods' do
|
|
424
424
|
describe '.faye' do
|
425
425
|
subject { client.send(:faye) }
|
426
426
|
|
427
|
-
context 'with missing oauth token' do
|
428
|
-
let(:instance_url) { 'http://foobar' }
|
429
|
-
let(:oauth_token) { nil }
|
430
|
-
specify { expect { subject }.to raise_error RuntimeError, 'OAuth token missing. Call .authenticate! first.' }
|
431
|
-
end
|
432
|
-
|
433
427
|
context 'with missing instance url' do
|
434
428
|
let(:instance_url) { nil }
|
435
429
|
specify { expect { subject }.to raise_error RuntimeError, 'Instance URL missing. Call .authenticate! first.' }
|
data/spec/lib/sobject_spec.rb
CHANGED
@@ -59,7 +59,10 @@ describe Restforce::SObject do
|
|
59
59
|
context 'when an Id is present' do
|
60
60
|
before do
|
61
61
|
hash.merge!(:Id => '001D000000INjVe')
|
62
|
-
@request = stub_api_request 'sobjects/Whizbang/001D000000INjVe',
|
62
|
+
@request = stub_api_request 'sobjects/Whizbang/001D000000INjVe',
|
63
|
+
:method => :patch,
|
64
|
+
:body => "{\"Checkbox_Label\":false,\"Text_Label\":\"Hi there!\",\"Date_Label\":\"2010-01-01\"," +
|
65
|
+
"\"DateTime_Label\":\"2011-07-07T00:37:00.000+0000\",\"Picklist_Multiselect_Label\":\"four;six\"}"
|
63
66
|
end
|
64
67
|
|
65
68
|
after do
|
@@ -70,6 +73,26 @@ describe Restforce::SObject do
|
|
70
73
|
end
|
71
74
|
end
|
72
75
|
|
76
|
+
describe '.save!' do
|
77
|
+
subject { sobject.save! }
|
78
|
+
|
79
|
+
context 'when an exception is raised' do
|
80
|
+
before do
|
81
|
+
hash.merge!(:Id => '001D000000INjVe')
|
82
|
+
@request = stub_api_request 'sobjects/Whizbang/001D000000INjVe',
|
83
|
+
:with => 'sobject/delete_error_response',
|
84
|
+
:method => :patch,
|
85
|
+
:status => 404
|
86
|
+
end
|
87
|
+
|
88
|
+
after do
|
89
|
+
@request.should have_been_requested
|
90
|
+
end
|
91
|
+
|
92
|
+
specify { expect { subject }.to raise_error Faraday::Error::ResourceNotFound }
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
73
96
|
describe '.destroy' do
|
74
97
|
subject { sobject.destroy }
|
75
98
|
|
@@ -91,9 +114,30 @@ describe Restforce::SObject do
|
|
91
114
|
end
|
92
115
|
end
|
93
116
|
|
117
|
+
describe '.destroy!' do
|
118
|
+
subject { sobject.destroy! }
|
119
|
+
|
120
|
+
context 'when an exception is raised' do
|
121
|
+
before do
|
122
|
+
hash.merge!(:Id => '001D000000INjVe')
|
123
|
+
@request = stub_api_request 'sobjects/Whizbang/001D000000INjVe',
|
124
|
+
:with => 'sobject/delete_error_response',
|
125
|
+
:method => :delete,
|
126
|
+
:status => 404
|
127
|
+
end
|
128
|
+
|
129
|
+
after do
|
130
|
+
@request.should have_been_requested
|
131
|
+
end
|
132
|
+
|
133
|
+
specify { expect { subject }.to raise_error Faraday::Error::ResourceNotFound }
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
94
137
|
describe '.describe' do
|
95
138
|
before do
|
96
|
-
@request = stub_api_request 'sobjects/Whizbang/describe',
|
139
|
+
@request = stub_api_request 'sobjects/Whizbang/describe',
|
140
|
+
:with => 'sobject/sobject_describe_success_response'
|
97
141
|
end
|
98
142
|
|
99
143
|
after 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.1.
|
4
|
+
version: 0.1.9
|
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-
|
12
|
+
date: 2012-12-04 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rake
|
@@ -171,6 +171,12 @@ files:
|
|
171
171
|
- Rakefile
|
172
172
|
- lib/restforce.rb
|
173
173
|
- lib/restforce/client.rb
|
174
|
+
- lib/restforce/client/api.rb
|
175
|
+
- lib/restforce/client/authentication.rb
|
176
|
+
- lib/restforce/client/caching.rb
|
177
|
+
- lib/restforce/client/canvas.rb
|
178
|
+
- lib/restforce/client/connection.rb
|
179
|
+
- lib/restforce/client/streaming.rb
|
174
180
|
- lib/restforce/collection.rb
|
175
181
|
- lib/restforce/config.rb
|
176
182
|
- lib/restforce/mash.rb
|
@@ -251,12 +257,18 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
251
257
|
- - ! '>='
|
252
258
|
- !ruby/object:Gem::Version
|
253
259
|
version: '0'
|
260
|
+
segments:
|
261
|
+
- 0
|
262
|
+
hash: -2351979982713256813
|
254
263
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
255
264
|
none: false
|
256
265
|
requirements:
|
257
266
|
- - ! '>='
|
258
267
|
- !ruby/object:Gem::Version
|
259
268
|
version: '0'
|
269
|
+
segments:
|
270
|
+
- 0
|
271
|
+
hash: -2351979982713256813
|
260
272
|
requirements: []
|
261
273
|
rubyforge_project:
|
262
274
|
rubygems_version: 1.8.23
|