glg-databasedotcom 1.3.2.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 +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +262 -0
- data/lib/databasedotcom.rb +11 -0
- data/lib/databasedotcom/chatter.rb +11 -0
- data/lib/databasedotcom/chatter/comment.rb +10 -0
- data/lib/databasedotcom/chatter/conversation.rb +100 -0
- data/lib/databasedotcom/chatter/feed.rb +64 -0
- data/lib/databasedotcom/chatter/feed_item.rb +40 -0
- data/lib/databasedotcom/chatter/feeds.rb +5 -0
- data/lib/databasedotcom/chatter/filter_feed.rb +14 -0
- data/lib/databasedotcom/chatter/group.rb +45 -0
- data/lib/databasedotcom/chatter/group_membership.rb +9 -0
- data/lib/databasedotcom/chatter/like.rb +9 -0
- data/lib/databasedotcom/chatter/message.rb +29 -0
- data/lib/databasedotcom/chatter/photo_methods.rb +55 -0
- data/lib/databasedotcom/chatter/record.rb +122 -0
- data/lib/databasedotcom/chatter/subscription.rb +9 -0
- data/lib/databasedotcom/chatter/user.rb +153 -0
- data/lib/databasedotcom/client.rb +552 -0
- data/lib/databasedotcom/collection.rb +37 -0
- data/lib/databasedotcom/core_extensions.rb +1 -0
- data/lib/databasedotcom/core_extensions/string_extensions.rb +9 -0
- data/lib/databasedotcom/flow.rb +25 -0
- data/lib/databasedotcom/sales_force_error.rb +31 -0
- data/lib/databasedotcom/sobject.rb +2 -0
- data/lib/databasedotcom/sobject/sobject.rb +376 -0
- data/lib/databasedotcom/version.rb +3 -0
- metadata +154 -0
@@ -0,0 +1,552 @@
|
|
1
|
+
require 'net/https'
|
2
|
+
require 'json'
|
3
|
+
require 'net/http/post/multipart'
|
4
|
+
require 'date'
|
5
|
+
|
6
|
+
module Databasedotcom
|
7
|
+
# Interface for operating the Force.com REST API
|
8
|
+
class Client
|
9
|
+
# The client id (aka "Consumer Key") to use for OAuth2 authentication
|
10
|
+
attr_accessor :client_id
|
11
|
+
# The client secret (aka "Consumer Secret" to use for OAuth2 authentication)
|
12
|
+
attr_accessor :client_secret
|
13
|
+
# The OAuth access token in use by the client
|
14
|
+
attr_accessor :oauth_token
|
15
|
+
# The OAuth refresh token in use by the client
|
16
|
+
attr_accessor :refresh_token
|
17
|
+
# The base URL to the authenticated user's SalesForce instance
|
18
|
+
attr_accessor :instance_url
|
19
|
+
# If true, print API debugging information to stdout. Defaults to false.
|
20
|
+
attr_accessor :debugging
|
21
|
+
# The host to use for OAuth2 authentication. Defaults to +login.salesforce.com+
|
22
|
+
attr_accessor :host
|
23
|
+
# The API version the client is using. Defaults to 23.0
|
24
|
+
attr_accessor :version
|
25
|
+
# A Module in which to materialize Sobject classes. Defaults to the global module (Object)
|
26
|
+
attr_accessor :sobject_module
|
27
|
+
# The SalesForce user id of the authenticated user
|
28
|
+
attr_reader :user_id
|
29
|
+
# The SalesForce username
|
30
|
+
attr_accessor :username
|
31
|
+
# The SalesForce password
|
32
|
+
attr_accessor :password
|
33
|
+
# The SalesForce organization id for the authenticated user's Salesforce instance
|
34
|
+
attr_reader :org_id
|
35
|
+
# The CA file configured for this instance, if any
|
36
|
+
attr_accessor :ca_file
|
37
|
+
# The SSL verify mode configured for this instance, if any
|
38
|
+
attr_accessor :verify_mode
|
39
|
+
|
40
|
+
# Returns a new client object. _options_ can be one of the following
|
41
|
+
#
|
42
|
+
# * A String containing the name of a YAML file formatted like:
|
43
|
+
# ---
|
44
|
+
# client_id: <your_salesforce_client_id>
|
45
|
+
# client_secret: <your_salesforce_client_secret>
|
46
|
+
# host: login.salesforce.com
|
47
|
+
# debugging: true
|
48
|
+
# version: 23.0
|
49
|
+
# sobject_module: My::Module
|
50
|
+
# ca_file: some/ca/file.cert
|
51
|
+
# verify_mode: OpenSSL::SSL::VERIFY_PEER
|
52
|
+
# * A Hash containing the following keys:
|
53
|
+
# client_id
|
54
|
+
# client_secret
|
55
|
+
# host
|
56
|
+
# debugging
|
57
|
+
# version
|
58
|
+
# sobject_module
|
59
|
+
# ca_file
|
60
|
+
# verify_mode
|
61
|
+
# If the environment variables DATABASEDOTCOM_CLIENT_ID, DATABASEDOTCOM_CLIENT_SECRET, DATABASEDOTCOM_HOST,
|
62
|
+
# DATABASEDOTCOM_DEBUGGING, DATABASEDOTCOM_VERSION, DATABASEDOTCOM_SOBJECT_MODULE, DATABASEDOTCOM_CA_FILE, and/or
|
63
|
+
# DATABASEDOTCOM_VERIFY_MODE are present, they override any other values provided
|
64
|
+
def initialize(options = {})
|
65
|
+
if options.is_a?(String)
|
66
|
+
@options = YAML.load_file(options)
|
67
|
+
@options["verify_mode"] = @options["verify_mode"].constantize if @options["verify_mode"] && @options["verify_mode"].is_a?(String)
|
68
|
+
else
|
69
|
+
@options = options
|
70
|
+
end
|
71
|
+
@options.symbolize_keys!
|
72
|
+
|
73
|
+
if ENV['DATABASE_COM_URL']
|
74
|
+
url = URI.parse(ENV['DATABASE_COM_URL'])
|
75
|
+
url_options = Hash[url.query.split("&").map{|q| q.split("=")}].symbolize_keys!
|
76
|
+
self.host = url.host
|
77
|
+
self.client_id = url_options[:oauth_key]
|
78
|
+
self.client_secret = url_options[:oauth_secret]
|
79
|
+
self.username = url_options[:user]
|
80
|
+
self.password = url_options[:password]
|
81
|
+
else
|
82
|
+
self.client_id = ENV['DATABASEDOTCOM_CLIENT_ID'] || @options[:client_id]
|
83
|
+
self.client_secret = ENV['DATABASEDOTCOM_CLIENT_SECRET'] || @options[:client_secret]
|
84
|
+
self.host = ENV['DATABASEDOTCOM_HOST'] || @options[:host] || "login.salesforce.com"
|
85
|
+
end
|
86
|
+
|
87
|
+
self.debugging = ENV['DATABASEDOTCOM_DEBUGGING'] || @options[:debugging]
|
88
|
+
self.version = ENV['DATABASEDOTCOM_VERSION'] || @options[:version]
|
89
|
+
self.version = self.version.to_s if self.version
|
90
|
+
self.sobject_module = ENV['DATABASEDOTCOM_SOBJECT_MODULE'] || @options[:sobject_module]
|
91
|
+
self.ca_file = ENV['DATABASEDOTCOM_CA_FILE'] || @options[:ca_file]
|
92
|
+
self.verify_mode = ENV['DATABASEDOTCOM_VERIFY_MODE'] || @options[:verify_mode]
|
93
|
+
self.verify_mode = self.verify_mode.to_i if self.verify_mode
|
94
|
+
end
|
95
|
+
|
96
|
+
# Authenticate to the Force.com API. _options_ is a Hash, interpreted as follows:
|
97
|
+
#
|
98
|
+
# * If _options_ contains the keys <tt>:username</tt> and <tt>:password</tt>, those credentials are used to authenticate. In this case, the value of <tt>:password</tt> may need to include a concatenated security token, if required by your Salesforce org
|
99
|
+
# * If _options_ contains the key <tt>:provider</tt>, it is assumed to be the hash returned by Omniauth from a successful web-based OAuth2 authentication
|
100
|
+
# * If _options_ contains the keys <tt>:token</tt> and <tt>:instance_url</tt>, those are assumed to be a valid OAuth2 token and instance URL for a Salesforce account, obtained from an external source. _options_ may also optionally contain the key <tt>:refresh_token</tt>
|
101
|
+
#
|
102
|
+
# Raises SalesForceError if an error occurs
|
103
|
+
def authenticate(options = nil)
|
104
|
+
if user_and_pass?(options)
|
105
|
+
req = https_request(self.host)
|
106
|
+
user = self.username || options[:username]
|
107
|
+
pass = self.password || options[:password]
|
108
|
+
path = encode_path_with_params('/services/oauth2/token', :grant_type => 'password', :client_id => self.client_id, :client_secret => self.client_secret, :username => user, :password => pass)
|
109
|
+
log_request("https://#{self.host}/#{path}")
|
110
|
+
result = req.post(path, "")
|
111
|
+
log_response(result)
|
112
|
+
raise SalesForceError.new(result) unless result.is_a?(Net::HTTPOK)
|
113
|
+
self.username = user
|
114
|
+
self.password = pass
|
115
|
+
parse_auth_response(result.body)
|
116
|
+
elsif options.is_a?(Hash)
|
117
|
+
if options.has_key?("provider")
|
118
|
+
parse_user_id_and_org_id_from_identity_url(options["uid"])
|
119
|
+
self.instance_url = options["credentials"]["instance_url"]
|
120
|
+
self.oauth_token = options["credentials"]["token"]
|
121
|
+
self.refresh_token = options["credentials"]["refresh_token"]
|
122
|
+
else
|
123
|
+
raise ArgumentError unless options.has_key?(:token) && options.has_key?(:instance_url)
|
124
|
+
self.instance_url = options[:instance_url]
|
125
|
+
self.oauth_token = options[:token]
|
126
|
+
self.refresh_token = options[:refresh_token]
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
self.version = "22.0" unless self.version
|
131
|
+
|
132
|
+
self.oauth_token
|
133
|
+
end
|
134
|
+
|
135
|
+
# The SalesForce organization id for the authenticated user's Salesforce instance
|
136
|
+
def org_id
|
137
|
+
@org_id ||= query_org_id # lazy query org_id when not set by login response
|
138
|
+
end
|
139
|
+
|
140
|
+
# Returns an Array of Strings listing the class names for every type of _Sobject_ in the database. Raises SalesForceError if an error occurs.
|
141
|
+
def list_sobjects
|
142
|
+
result = http_get("/services/data/v#{self.version}/sobjects")
|
143
|
+
if result.is_a?(Net::HTTPOK)
|
144
|
+
JSON.parse(result.body)["sobjects"].collect { |sobject| sobject["name"] }
|
145
|
+
elsif result.is_a?(Net::HTTPBadRequest)
|
146
|
+
raise SalesForceError.new(result)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
# Dynamically defines classes for Force.com class names. _classnames_ can be a single String or an Array of Strings. Returns the class or Array of classes defined.
|
151
|
+
#
|
152
|
+
# client.materialize("Contact") #=> Contact
|
153
|
+
# client.materialize(%w(Contact Company)) #=> [Contact, Company]
|
154
|
+
#
|
155
|
+
# The classes defined by materialize derive from Sobject, and have getters and setters defined for all the attributes defined by the associated Force.com Sobject.
|
156
|
+
def materialize(classnames)
|
157
|
+
classes = (classnames.is_a?(Array) ? classnames : [classnames]).collect do |clazz|
|
158
|
+
original_classname = clazz
|
159
|
+
clazz = original_classname[0,1].capitalize + original_classname[1..-1]
|
160
|
+
unless const_defined_in_module(module_namespace, clazz)
|
161
|
+
new_class = module_namespace.const_set(clazz, Class.new(Databasedotcom::Sobject::Sobject))
|
162
|
+
new_class.client = self
|
163
|
+
new_class.materialize(original_classname)
|
164
|
+
new_class
|
165
|
+
else
|
166
|
+
module_namespace.const_get(clazz)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
classes.length == 1 ? classes.first : classes
|
171
|
+
end
|
172
|
+
|
173
|
+
# Returns an Array of Hashes listing the properties for every type of _Sobject_ in the database. Raises SalesForceError if an error occurs.
|
174
|
+
def describe_sobjects
|
175
|
+
result = http_get("/services/data/v#{self.version}/sobjects")
|
176
|
+
JSON.parse(result.body)["sobjects"]
|
177
|
+
end
|
178
|
+
|
179
|
+
# Returns a description of the Sobject specified by _class_name_. The description includes all fields and their properties for the Sobject.
|
180
|
+
def describe_sobject(class_name)
|
181
|
+
result = http_get("/services/data/v#{self.version}/sobjects/#{class_name}/describe")
|
182
|
+
JSON.parse(result.body)
|
183
|
+
end
|
184
|
+
|
185
|
+
# Returns an instance of the Sobject specified by _class_or_classname_ (which can be either a String or a Class) populated with the values of the Force.com record specified by _record_id_.
|
186
|
+
# If given a Class that is not defined, it will attempt to materialize the class on demand.
|
187
|
+
#
|
188
|
+
# client.find(Account, "recordid") #=> #<Account @Id="recordid", ...>
|
189
|
+
def find(class_or_classname, record_id)
|
190
|
+
class_or_classname = find_or_materialize(class_or_classname)
|
191
|
+
result = http_get("/services/data/v#{self.version}/sobjects/#{class_or_classname.sobject_name}/#{record_id}")
|
192
|
+
response = JSON.parse(result.body)
|
193
|
+
new_record = class_or_classname.new
|
194
|
+
class_or_classname.description["fields"].each do |field|
|
195
|
+
set_value(new_record, field["name"], response[key_from_label(field["label"])] || response[field["name"]], field["type"])
|
196
|
+
end
|
197
|
+
new_record
|
198
|
+
end
|
199
|
+
|
200
|
+
# Returns a Collection of Sobjects of the class specified in the _soql_expr_, which is a valid SOQL[http://www.salesforce.com/us/developer/docs/api/Content/sforce_api_calls_soql.htm] expression. The objects will only be populated with the values of attributes specified in the query.
|
201
|
+
#
|
202
|
+
# client.query("SELECT Name FROM Account") #=> [#<Account @Id=nil, @Name="Foo", ...>, #<Account @Id=nil, @Name="Bar", ...> ...]
|
203
|
+
def query(soql_expr)
|
204
|
+
result = http_get("/services/data/v#{self.version}/query", :q => soql_expr)
|
205
|
+
collection_from(result.body)
|
206
|
+
end
|
207
|
+
|
208
|
+
# Returns a Collection of Sobject instances form the results of the SOSL[http://www.salesforce.com/us/developer/docs/api/Content/sforce_api_calls_sosl.htm] search.
|
209
|
+
#
|
210
|
+
# client.search("FIND {bar}") #=> [#<Account @Name="foobar", ...>, #<Account @Name="barfoo", ...> ...]
|
211
|
+
def search(sosl_expr)
|
212
|
+
result = http_get("/services/data/v#{self.version}/search", :q => sosl_expr)
|
213
|
+
collection_from(result.body)
|
214
|
+
end
|
215
|
+
|
216
|
+
# Used by Collection objects. Returns a Collection of Sobjects from the specified URL path that represents the next page of paginated results.
|
217
|
+
def next_page(path)
|
218
|
+
result = http_get(path)
|
219
|
+
collection_from(result.body)
|
220
|
+
end
|
221
|
+
|
222
|
+
# Used by Collection objects. Returns a Collection of Sobjects from the specified URL path that represents the previous page of paginated results.
|
223
|
+
def previous_page(path)
|
224
|
+
result = http_get(path)
|
225
|
+
collection_from(result.body)
|
226
|
+
end
|
227
|
+
|
228
|
+
# Returns a new instance of _class_or_classname_ (which can be passed in as either a String or a Class) with the specified attributes.
|
229
|
+
#
|
230
|
+
# client.create("Car", {"Color" => "Blue", "Year" => "2011"}) #=> #<Car @Id="recordid", @Color="Blue", @Year="2011">
|
231
|
+
def create(class_or_classname, object_attrs)
|
232
|
+
class_or_classname = find_or_materialize(class_or_classname)
|
233
|
+
json_for_assignment = coerced_json(object_attrs, class_or_classname)
|
234
|
+
result = http_post("/services/data/v#{self.version}/sobjects/#{class_or_classname.sobject_name}", json_for_assignment)
|
235
|
+
new_object = class_or_classname.new
|
236
|
+
JSON.parse(json_for_assignment).each do |property, value|
|
237
|
+
set_value(new_object, property, value, class_or_classname.type_map[property][:type])
|
238
|
+
end
|
239
|
+
id = JSON.parse(result.body)["id"]
|
240
|
+
set_value(new_object, "Id", id, "id")
|
241
|
+
new_object
|
242
|
+
end
|
243
|
+
|
244
|
+
# Updates the attributes of the record of type _class_or_classname_ and specified by _record_id_ with the values of _new_attrs_ in the Force.com database. _new_attrs_ is a hash of attribute => value
|
245
|
+
#
|
246
|
+
# client.update("Car", "rid", {"Color" => "Red"})
|
247
|
+
def update(class_or_classname, record_id, new_attrs)
|
248
|
+
class_or_classname = find_or_materialize(class_or_classname)
|
249
|
+
json_for_update = coerced_json(new_attrs, class_or_classname)
|
250
|
+
http_patch("/services/data/v#{self.version}/sobjects/#{class_or_classname.sobject_name}/#{record_id}", json_for_update)
|
251
|
+
end
|
252
|
+
|
253
|
+
# Attempts to find the record on Force.com of type _class_or_classname_ with attribute _field_ set as _value_. If found, it will update the record with the _attrs_ hash.
|
254
|
+
# If not found, it will create a new record with _attrs_.
|
255
|
+
#
|
256
|
+
# client.upsert(Car, "Color", "Blue", {"Year" => "2012"})
|
257
|
+
def upsert(class_or_classname, field, value, attrs)
|
258
|
+
clazz = find_or_materialize(class_or_classname)
|
259
|
+
json_for_update = coerced_json(attrs, clazz)
|
260
|
+
http_patch("/services/data/v#{self.version}/sobjects/#{clazz.sobject_name}/#{field}/#{value}", json_for_update)
|
261
|
+
end
|
262
|
+
|
263
|
+
# Deletes the record of type _class_or_classname_ with id of _record_id_. _class_or_classname_ can be a String or a Class.
|
264
|
+
#
|
265
|
+
# client.delete(Car, "rid")
|
266
|
+
def delete(class_or_classname, record_id)
|
267
|
+
clazz = find_or_materialize(class_or_classname)
|
268
|
+
http_delete("/services/data/v#{self.version}/sobjects/#{clazz.sobject_name}/#{record_id}")
|
269
|
+
end
|
270
|
+
|
271
|
+
# Returns a Collection of recently touched items. The Collection contains Sobject instances that are fully populated with their correct values.
|
272
|
+
def recent
|
273
|
+
result = http_get("/services/data/v#{self.version}/recent")
|
274
|
+
collection_from(result.body)
|
275
|
+
end
|
276
|
+
|
277
|
+
# Returns an array of trending topic names.
|
278
|
+
def trending_topics
|
279
|
+
result = http_get("/services/data/v#{self.version}/chatter/topics/trending")
|
280
|
+
result = JSON.parse(result.body)
|
281
|
+
result["topics"].collect { |topic| topic["name"] }
|
282
|
+
end
|
283
|
+
|
284
|
+
# Performs an HTTP GET request to the specified path (relative to self.instance_url). Query parameters are included from _parameters_. The required
|
285
|
+
# +Authorization+ header is automatically included, as are any additional headers specified in _headers_. Returns the HTTPResult if it is of type
|
286
|
+
# HTTPSuccess- raises SalesForceError otherwise.
|
287
|
+
def http_get(path, parameters={}, headers={})
|
288
|
+
with_encoded_path_and_checked_response(path, parameters) do |encoded_path|
|
289
|
+
https_request.get(encoded_path, {"Authorization" => "OAuth #{self.oauth_token}"}.merge(headers))
|
290
|
+
end
|
291
|
+
end
|
292
|
+
|
293
|
+
|
294
|
+
# Performs an HTTP DELETE request to the specified path (relative to self.instance_url). Query parameters are included from _parameters_. The required
|
295
|
+
# +Authorization+ header is automatically included, as are any additional headers specified in _headers_. Returns the HTTPResult if it is of type
|
296
|
+
# HTTPSuccess- raises SalesForceError otherwise.
|
297
|
+
def http_delete(path, parameters={}, headers={})
|
298
|
+
with_encoded_path_and_checked_response(path, parameters, {:expected_result_class => Net::HTTPNoContent}) do |encoded_path|
|
299
|
+
https_request.delete(encoded_path, {"Authorization" => "OAuth #{self.oauth_token}"}.merge(headers))
|
300
|
+
end
|
301
|
+
end
|
302
|
+
|
303
|
+
# Performs an HTTP POST request to the specified path (relative to self.instance_url). The body of the request is taken from _data_.
|
304
|
+
# Query parameters are included from _parameters_. The required +Authorization+ header is automatically included, as are any additional
|
305
|
+
# headers specified in _headers_. Returns the HTTPResult if it is of type HTTPSuccess- raises SalesForceError otherwise.
|
306
|
+
def http_post(path, data=nil, parameters={}, headers={})
|
307
|
+
with_encoded_path_and_checked_response(path, parameters, {:data => data}) do |encoded_path|
|
308
|
+
https_request.post(encoded_path, data, {"Content-Type" => data ? "application/json" : "text/plain", "Authorization" => "OAuth #{self.oauth_token}"}.merge(headers))
|
309
|
+
end
|
310
|
+
end
|
311
|
+
|
312
|
+
# Performs an HTTP PATCH request to the specified path (relative to self.instance_url). The body of the request is taken from _data_.
|
313
|
+
# Query parameters are included from _parameters_. The required +Authorization+ header is automatically included, as are any additional
|
314
|
+
# headers specified in _headers_. Returns the HTTPResult if it is of type HTTPSuccess- raises SalesForceError otherwise.
|
315
|
+
def http_patch(path, data=nil, parameters={}, headers={})
|
316
|
+
with_encoded_path_and_checked_response(path, parameters, {:data => data}) do |encoded_path|
|
317
|
+
https_request.send_request("PATCH", encoded_path, data, {"Content-Type" => data ? "application/json" : "text/plain", "Authorization" => "OAuth #{self.oauth_token}"}.merge(headers))
|
318
|
+
end
|
319
|
+
end
|
320
|
+
|
321
|
+
# Performs an HTTP POST request to the specified path (relative to self.instance_url), using Content-Type multiplart/form-data.
|
322
|
+
# The parts of the body of the request are taken from parts_. Query parameters are included from _parameters_. The required
|
323
|
+
# +Authorization+ header is automatically included, as are any additional headers specified in _headers_.
|
324
|
+
# Returns the HTTPResult if it is of type HTTPSuccess- raises SalesForceError otherwise.
|
325
|
+
def http_multipart_post(path, parts, parameters={}, headers={})
|
326
|
+
with_encoded_path_and_checked_response(path, parameters) do |encoded_path|
|
327
|
+
https_request.request(Net::HTTP::Post::Multipart.new(encoded_path, parts, {"Authorization" => "OAuth #{self.oauth_token}"}.merge(headers)))
|
328
|
+
end
|
329
|
+
end
|
330
|
+
|
331
|
+
private
|
332
|
+
|
333
|
+
def with_encoded_path_and_checked_response(path, parameters, options = {})
|
334
|
+
ensure_expected_response(options[:expected_result_class]) do
|
335
|
+
with_logging(encode_path_with_params(path, parameters), options) do |encoded_path|
|
336
|
+
yield(encoded_path)
|
337
|
+
end
|
338
|
+
end
|
339
|
+
end
|
340
|
+
|
341
|
+
def with_logging(encoded_path, options)
|
342
|
+
log_request(encoded_path, options)
|
343
|
+
response = yield encoded_path
|
344
|
+
log_response(response)
|
345
|
+
response
|
346
|
+
end
|
347
|
+
|
348
|
+
def ensure_expected_response(expected_result_class)
|
349
|
+
response = yield
|
350
|
+
|
351
|
+
unless response.is_a?(expected_result_class || Net::HTTPSuccess)
|
352
|
+
if response.is_a?(Net::HTTPUnauthorized)
|
353
|
+
if self.refresh_token
|
354
|
+
response = with_encoded_path_and_checked_response("/services/oauth2/token", { :grant_type => "refresh_token", :refresh_token => self.refresh_token, :client_id => self.client_id, :client_secret => self.client_secret}, :host => self.host) do |encoded_path|
|
355
|
+
response = https_request(self.host).post(encoded_path, nil)
|
356
|
+
if response.is_a?(Net::HTTPOK)
|
357
|
+
parse_auth_response(response.body)
|
358
|
+
end
|
359
|
+
response
|
360
|
+
end
|
361
|
+
elsif self.username && self.password
|
362
|
+
response = with_encoded_path_and_checked_response("/services/oauth2/token", { :grant_type => "password", :username => self.username, :password => self.password, :client_id => self.client_id, :client_secret => self.client_secret}, :host => self.host) do |encoded_path|
|
363
|
+
response = https_request(self.host).post(encoded_path, nil)
|
364
|
+
if response.is_a?(Net::HTTPOK)
|
365
|
+
parse_auth_response(response.body)
|
366
|
+
end
|
367
|
+
response
|
368
|
+
end
|
369
|
+
end
|
370
|
+
|
371
|
+
if response.is_a?(Net::HTTPSuccess)
|
372
|
+
response = yield
|
373
|
+
end
|
374
|
+
end
|
375
|
+
|
376
|
+
raise SalesForceError.new(response) unless response.is_a?(expected_result_class || Net::HTTPSuccess)
|
377
|
+
end
|
378
|
+
|
379
|
+
response
|
380
|
+
end
|
381
|
+
|
382
|
+
def https_request(host=nil)
|
383
|
+
Net::HTTP.new(host || URI.parse(self.instance_url).host, 443).tap do |http|
|
384
|
+
http.use_ssl = true
|
385
|
+
http.ca_file = self.ca_file if self.ca_file
|
386
|
+
http.verify_mode = self.verify_mode if self.verify_mode
|
387
|
+
end
|
388
|
+
end
|
389
|
+
|
390
|
+
def encode_path_with_params(path, parameters={})
|
391
|
+
[URI.escape(path), encode_parameters(parameters)].reject{|el| el.empty?}.join('?')
|
392
|
+
end
|
393
|
+
|
394
|
+
def encode_parameters(parameters={})
|
395
|
+
(parameters || {}).collect { |k, v| "#{uri_escape(k)}=#{uri_escape(v)}" }.join('&')
|
396
|
+
end
|
397
|
+
|
398
|
+
def log_request(path, options={})
|
399
|
+
base_url = options[:host] ? "https://#{options[:host]}" : self.instance_url
|
400
|
+
puts "***** REQUEST: #{path.include?(':') ? path : URI.join(base_url, path)}#{options[:data] ? " => #{options[:data]}" : ''}" if self.debugging
|
401
|
+
end
|
402
|
+
|
403
|
+
def uri_escape(str)
|
404
|
+
URI.escape(str.to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
|
405
|
+
end
|
406
|
+
|
407
|
+
def log_response(result)
|
408
|
+
puts "***** RESPONSE: #{result.class.name} -> #{result.body}" if self.debugging
|
409
|
+
end
|
410
|
+
|
411
|
+
def find_or_materialize(class_or_classname)
|
412
|
+
if class_or_classname.is_a?(Class)
|
413
|
+
clazz = class_or_classname
|
414
|
+
else
|
415
|
+
match = class_or_classname.match(/(?:(.+)::)?(\w+)$/)
|
416
|
+
preceding_namespace = match[1]
|
417
|
+
classname = match[2]
|
418
|
+
raise ArgumentError if preceding_namespace && preceding_namespace != module_namespace.name
|
419
|
+
clazz = module_namespace.const_get(classname.to_sym) rescue nil
|
420
|
+
clazz ||= self.materialize(classname)
|
421
|
+
end
|
422
|
+
clazz
|
423
|
+
end
|
424
|
+
|
425
|
+
def module_namespace
|
426
|
+
_module = self.sobject_module
|
427
|
+
_module = _module.constantize if _module.is_a? String
|
428
|
+
_module || Object
|
429
|
+
end
|
430
|
+
|
431
|
+
def collection_from(response)
|
432
|
+
response = JSON.parse(response)
|
433
|
+
collection_from_hash( response )
|
434
|
+
end
|
435
|
+
|
436
|
+
# Converts a Hash of object data into a concrete SObject
|
437
|
+
def record_from_hash(data)
|
438
|
+
attributes = data.delete('attributes')
|
439
|
+
new_record = find_or_materialize(attributes["type"]).new
|
440
|
+
data.each do |name, value|
|
441
|
+
field = new_record.description['fields'].find do |field|
|
442
|
+
key_from_label(field["label"]) == name || field["name"] == name || field["relationshipName"] == name
|
443
|
+
end
|
444
|
+
|
445
|
+
# Field not found
|
446
|
+
if field == nil
|
447
|
+
break
|
448
|
+
end
|
449
|
+
|
450
|
+
# If reference/lookup field data was fetched, recursively build the child record and apply
|
451
|
+
if value.is_a?(Hash) and field['type'] == 'reference' and field["relationshipName"]
|
452
|
+
relation = record_from_hash( value )
|
453
|
+
set_value( new_record, field["relationshipName"], relation, 'reference' )
|
454
|
+
|
455
|
+
# Apply the raw value for all other field types
|
456
|
+
else
|
457
|
+
set_value(new_record, field["name"], value, field["type"]) if field
|
458
|
+
end
|
459
|
+
end
|
460
|
+
new_record
|
461
|
+
end
|
462
|
+
|
463
|
+
def collection_from_hash(data)
|
464
|
+
array_response = data.is_a?(Array)
|
465
|
+
if array_response
|
466
|
+
records = data.collect { |rec| self.find(rec["attributes"]["type"], rec["Id"]) }
|
467
|
+
else
|
468
|
+
records = data["records"].collect do |record|
|
469
|
+
record_from_hash( record )
|
470
|
+
end
|
471
|
+
end
|
472
|
+
|
473
|
+
Databasedotcom::Collection.new(self, array_response ? records.length : data["totalSize"], array_response ? nil : data["nextRecordsUrl"]).concat(records)
|
474
|
+
end
|
475
|
+
|
476
|
+
def set_value(record, attr, value, attr_type)
|
477
|
+
value_to_set = value
|
478
|
+
|
479
|
+
case attr_type
|
480
|
+
when "datetime"
|
481
|
+
value_to_set = DateTime.parse(value) rescue nil
|
482
|
+
|
483
|
+
when "date"
|
484
|
+
value_to_set = Date.parse(value) rescue nil
|
485
|
+
|
486
|
+
when "multipicklist"
|
487
|
+
value_to_set = value.split(";") rescue []
|
488
|
+
end
|
489
|
+
|
490
|
+
record.send("#{attr}=", value_to_set)
|
491
|
+
end
|
492
|
+
|
493
|
+
def coerced_json(attrs, clazz)
|
494
|
+
if attrs.is_a?(Hash)
|
495
|
+
coerced_attrs = {}
|
496
|
+
attrs.keys.each do |key|
|
497
|
+
case clazz.field_type(key.to_s)
|
498
|
+
when "multipicklist"
|
499
|
+
coerced_attrs[key] = (attrs[key] || []).join(';')
|
500
|
+
when "datetime"
|
501
|
+
begin
|
502
|
+
attrs[key] = DateTime.parse(attrs[key]) if attrs[key].is_a?(String)
|
503
|
+
coerced_attrs[key] = attrs[key].strftime(RUBY_VERSION.match(/^1.8/) ? "%Y-%m-%dT%H:%M:%S.000%z" : "%Y-%m-%dT%H:%M:%S.%L%z")
|
504
|
+
rescue
|
505
|
+
nil
|
506
|
+
end
|
507
|
+
when "date"
|
508
|
+
if attrs[key]
|
509
|
+
coerced_attrs[key] = attrs[key].respond_to?(:strftime) ? attrs[key].strftime("%Y-%m-%d") : attrs[key]
|
510
|
+
else
|
511
|
+
coerced_attrs[key] = nil
|
512
|
+
end
|
513
|
+
else
|
514
|
+
coerced_attrs[key] = attrs[key]
|
515
|
+
end
|
516
|
+
end
|
517
|
+
coerced_attrs.to_json
|
518
|
+
else
|
519
|
+
attrs
|
520
|
+
end
|
521
|
+
end
|
522
|
+
|
523
|
+
def key_from_label(label)
|
524
|
+
label.gsub(' ', '_')
|
525
|
+
end
|
526
|
+
|
527
|
+
def user_and_pass?(options)
|
528
|
+
(self.username && self.password) || (options && options[:username] && options[:password])
|
529
|
+
end
|
530
|
+
|
531
|
+
def parse_user_id_and_org_id_from_identity_url(identity_url)
|
532
|
+
m = identity_url.match(/\/id\/([^\/]+)\/([^\/]+)$/)
|
533
|
+
@org_id = m[1] rescue nil
|
534
|
+
@user_id = m[2] rescue nil
|
535
|
+
end
|
536
|
+
|
537
|
+
def parse_auth_response(body)
|
538
|
+
json = JSON.parse(body)
|
539
|
+
parse_user_id_and_org_id_from_identity_url(json["id"])
|
540
|
+
self.instance_url = json["instance_url"]
|
541
|
+
self.oauth_token = json["access_token"]
|
542
|
+
end
|
543
|
+
|
544
|
+
def query_org_id
|
545
|
+
query("select id from Organization")[0]["Id"]
|
546
|
+
end
|
547
|
+
|
548
|
+
def const_defined_in_module(mod, const)
|
549
|
+
mod.method(:const_defined?).arity == 1 ? mod.const_defined?(const) : mod.const_defined?(const, false)
|
550
|
+
end
|
551
|
+
end
|
552
|
+
end
|