Class: Databasedotcom::Client

Inherits:
Object
  • Object
show all
Defined in:
lib/databasedotcom/client.rb

Overview

Interface for operating the Force.com REST API

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(options = {}) ⇒ Client

Returns a new client object. options can be one of the following

  • A String containing the name of a YAML file formatted like:

    ---
    client_id: <your_salesforce_client_id>
    client_secret: <your_salesforce_client_secret>
    host: login.salesforce.com
    debugging: true
    version: 23.0
    sobject_module: My::Module
    
  • A Hash containing the following keys:

    client_id
    client_secret
    host
    debugging
    version
    sobject_module
    

If the environment variables DATABASEDOTCOM_CLIENT_ID, DATABASEDOTCOM_CLIENT_SECRET, DATABASEDOTCOM_HOST, DATABASEDOTCOM_DEBUGGING, DATABASEDOTCOM_VERSION, and/or DATABASEDOTCOM_SOBJECT_MODULE are present, they override any other values provided



51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'lib/databasedotcom/client.rb', line 51

def initialize(options = {})
    if options.is_a?(String)
      @options = YAML.load_file(options)
    else
      @options = options
    end
    @options.symbolize_keys!

    if ENV['DATABASE_COM_URL']
      url = URI.parse(ENV['DATABASE_COM_URL'])
      url_options = Hash[url.query.split("&").map{|q| q.split("=")}].symbolize_keys!
      self.host = url.host
      self.client_id = url_options[:oauth_key]
      self.client_secret = url_options[:oauth_secret]
      self.username = url_options[:user]
      self.password = url_options[:password]
    else
      self.client_id = ENV['DATABASEDOTCOM_CLIENT_ID'] || @options[:client_id]
      self.client_secret = ENV['DATABASEDOTCOM_CLIENT_SECRET'] || @options[:client_secret]
      self.host = ENV['DATABASEDOTCOM_HOST'] || @options[:host] || "login.salesforce.com"
    end
    self.debugging = ENV['DATABASEDOTCOM_DEBUGGING'] || @options[:debugging]
    self.version = ENV['DATABASEDOTCOM_VERSION'] || @options[:version]
    self.version = self.version.to_s if self.version
    self.sobject_module = ENV['DATABASEDOTCOM_SOBJECT_MODULE'] || @options[:sobject_module]
end

Instance Attribute Details

#client_idObject

The client id (aka “Consumer Key”) to use for OAuth2 authentication



9
10
11
# File 'lib/databasedotcom/client.rb', line 9

def client_id
  @client_id
end

#client_secretObject

The client secret (aka “Consumer Secret” to use for OAuth2 authentication)



11
12
13
# File 'lib/databasedotcom/client.rb', line 11

def client_secret
  @client_secret
end

#debuggingObject

If true, print API debugging information to stdout. Defaults to false.



17
18
19
# File 'lib/databasedotcom/client.rb', line 17

def debugging
  @debugging
end

#hostObject

The host to use for OAuth2 authentication. Defaults to login.salesforce.com



19
20
21
# File 'lib/databasedotcom/client.rb', line 19

def host
  @host
end

#instance_urlObject

The base URL to the authenticated user’s SalesForce instance



15
16
17
# File 'lib/databasedotcom/client.rb', line 15

def instance_url
  @instance_url
end

#oauth_tokenObject

The OAuth access token in use by the client



13
14
15
# File 'lib/databasedotcom/client.rb', line 13

def oauth_token
  @oauth_token
end

#passwordObject

The SalesForce password



29
30
31
# File 'lib/databasedotcom/client.rb', line 29

def password
  @password
end

#sobject_moduleObject

A Module in which to materialize Sobject classes. Defaults to the global module (Object)



23
24
25
# File 'lib/databasedotcom/client.rb', line 23

def sobject_module
  @sobject_module
end

#user_idObject (readonly)

The SalesForce user id of the authenticated user



25
26
27
# File 'lib/databasedotcom/client.rb', line 25

def user_id
  @user_id
end

#usernameObject

The SalesForce username



27
28
29
# File 'lib/databasedotcom/client.rb', line 27

def username
  @username
end

#versionObject

The API version the client is using. Defaults to 23.0



21
22
23
# File 'lib/databasedotcom/client.rb', line 21

def version
  @version
end

Instance Method Details

#authenticate(options = nil) ⇒ Object

Authenticate to the Force.com API. options is a Hash, interpreted as follows:

  • If options contains the keys :username and :password, those credentials are used to authenticate. In this case, the value of :password may need to include a concatenated security token, if required by your Salesforce org

  • If options contains the key :provider, it is assumed to be the hash returned by Omniauth from a successful web-based OAuth2 authentication

  • If options contains the keys :token and :instance_url, those are assumed to be a valid OAuth2 token and instance URL for a Salesforce account, obtained from an external source

Raises SalesForceError if an error occurs



85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/databasedotcom/client.rb', line 85

def authenticate(options = nil)
  if user_and_pass?(options)
    req = Net::HTTP.new(self.host, 443)
    req.use_ssl=true
    user = self.username || options[:username]
    pass = self.password || options[:password]
    path = "/services/oauth2/token?grant_type=password&client_id=#{self.client_id}&client_secret=#{client_secret}&username=#{user}&password=#{pass}"
    log_request("https://#{self.host}/#{path}")
    result = req.post(path, "")
    log_response(result)
    raise SalesForceError.new(result) unless result.is_a?(Net::HTTPOK)
    json = JSON.parse(result.body)
    @user_id = json["id"].match(/\/([^\/]+)$/)[1] rescue nil
    self.instance_url = json["instance_url"]
    self.oauth_token = json["access_token"]
  elsif options.is_a?(Hash)
    if options.has_key?("provider")
      @user_id = options["extra"]["user_hash"]["user_id"] rescue nil
      self.instance_url = options["credentials"]["instance_url"]
      self.oauth_token = options["credentials"]["token"]
    else
      raise ArgumentError unless options.has_key?(:token) && options.has_key?(:instance_url)
      self.instance_url = options[:instance_url]
      self.oauth_token = options[:token]
    end
  end

  self.version = "23.0" unless self.version

  self.oauth_token
end

#create(class_or_classname, object_attrs) ⇒ Object

Returns a new instance of class_or_classname (which can be passed in as either a String or a Class) with the specified attributes.

client.create("Car", {"Color" => "Blue", "Year" => "2011"}) #=> #<Car @Id="recordid", @Color="Blue", @Year="2011">


202
203
204
205
206
207
208
209
210
211
212
213
# File 'lib/databasedotcom/client.rb', line 202

def create(class_or_classname, object_attrs)
  class_or_classname = find_or_materialize(class_or_classname)
  json_for_assignment = coerced_json(object_attrs, class_or_classname)
  result = http_post("/services/data/v#{self.version}/sobjects/#{class_or_classname.sobject_name}", json_for_assignment)
  new_object = class_or_classname.new
  JSON.parse(json_for_assignment).each do |property, value|
    set_value(new_object, property, value, class_or_classname.type_map[property][:type])
  end
  id = JSON.parse(result.body)["id"]
  set_value(new_object, "Id", id, "id")
  new_object
end

#delete(class_or_classname, record_id) ⇒ Object

Deletes the record of type class_or_classname with id of record_id. class_or_classname can be a String or a Class.

client.delete(Car, "rid")


237
238
239
240
# File 'lib/databasedotcom/client.rb', line 237

def delete(class_or_classname, record_id)
  clazz = find_or_materialize(class_or_classname)
  http_delete("/services/data/v#{self.version}/sobjects/#{clazz.sobject_name}/#{record_id}")
end

#describe_sobject(class_name) ⇒ Object

Returns a description of the Sobject specified by class_name. The description includes all fields and their properties for the Sobject.



151
152
153
154
# File 'lib/databasedotcom/client.rb', line 151

def describe_sobject(class_name)
  result = http_get("/services/data/v#{self.version}/sobjects/#{class_name}/describe")
  JSON.parse(result.body)
end

#find(class_or_classname, record_id) ⇒ Object

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. If given a Class that is not defined, it will attempt to materialize the class on demand.

client.find(Account, "recordid") #=> #<Account @Id="recordid", ...>


160
161
162
163
164
165
166
167
168
169
# File 'lib/databasedotcom/client.rb', line 160

def find(class_or_classname, record_id)
  class_or_classname = find_or_materialize(class_or_classname)
  result = http_get("/services/data/v#{self.version}/sobjects/#{class_or_classname.sobject_name}/#{record_id}")
  response = JSON.parse(result.body)
  new_record = class_or_classname.new
  class_or_classname.description["fields"].each do |field|
    set_value(new_record, field["name"], response[key_from_label(field["label"])] || response[field["name"]], field["type"])
  end
  new_record
end

#http_delete(path, parameters = {}, headers = {}) ⇒ Object

Performs an HTTP DELETE request to the specified path (relative to self.instance_url). Query parameters are included from parameters. The required Authorization header is automatically included, as are any additional headers specified in headers. Returns the HTTPResult if it is of type HTTPSuccess- raises SalesForceError otherwise.

Raises:



274
275
276
277
278
279
280
281
282
283
284
# File 'lib/databasedotcom/client.rb', line 274

def http_delete(path, parameters={}, headers={})
  req = Net::HTTP.new(URI.parse(self.instance_url).host, 443)
  req.use_ssl = true
  path_parameters = (parameters || {}).collect { |k, v| "#{URI.escape(k.to_s)}=#{URI.escape(v.to_s)}" }.join('&')
  encoded_path = [URI.escape(path), path_parameters.empty? ? nil : path_parameters].compact.join('?')
  log_request(encoded_path)
  result = req.delete(encoded_path, {"Authorization" => "OAuth #{self.oauth_token}"}.merge(headers))
  log_response(result)
  raise SalesForceError.new(result) unless result.is_a?(Net::HTTPNoContent)
  result
end

#http_get(path, parameters = {}, headers = {}) ⇒ Object

Performs an HTTP GET request to the specified path (relative to self.instance_url). Query parameters are included from parameters. The required Authorization header is automatically included, as are any additional headers specified in headers. Returns the HTTPResult if it is of type HTTPSuccess- raises SalesForceError otherwise.

Raises:



258
259
260
261
262
263
264
265
266
267
268
# File 'lib/databasedotcom/client.rb', line 258

def http_get(path, parameters={}, headers={})
  req = Net::HTTP.new(URI.parse(self.instance_url).host, 443)
  req.use_ssl = true
  path_parameters = (parameters || {}).collect { |k, v| "#{URI.escape(k.to_s)}=#{URI.escape(v.to_s)}" }.join('&')
  encoded_path = [URI.escape(path), path_parameters.empty? ? nil : path_parameters].compact.join('?')
  log_request(encoded_path)
  result = req.get(encoded_path, {"Authorization" => "OAuth #{self.oauth_token}"}.merge(headers))
  log_response(result)
  raise SalesForceError.new(result) unless result.is_a?(Net::HTTPSuccess)
  result
end

#http_multipart_post(path, parts, parameters = {}, headers = {}) ⇒ Object

Performs an HTTP POST request to the specified path (relative to self.instance_url), using Content-Type multiplart/form-data. The parts of the body of the request are taken from parts_. Query parameters are included from parameters. The required Authorization header is automatically included, as are any additional headers specified in headers. Returns the HTTPResult if it is of type HTTPSuccess- raises SalesForceError otherwise.

Raises:



320
321
322
323
324
325
326
327
328
329
330
# File 'lib/databasedotcom/client.rb', line 320

def http_multipart_post(path, parts, parameters={}, headers={})
  req = Net::HTTP.new(URI.parse(self.instance_url).host, 443)
  req.use_ssl = true
  path_parameters = (parameters || {}).collect { |k, v| "#{URI.escape(k.to_s)}=#{URI.escape(v.to_s)}" }.join('&')
  encoded_path = [URI.escape(path), path_parameters.empty? ? nil : path_parameters].compact.join('?')
  log_request(encoded_path)
  result = req.request(Net::HTTP::Post::Multipart.new(encoded_path, parts, {"Authorization" => "OAuth #{self.oauth_token}"}.merge(headers)))
  log_response(result)
  raise SalesForceError.new(result) unless result.is_a?(Net::HTTPSuccess)
  result
end

#http_patch(path, data = nil, parameters = {}, headers = {}) ⇒ Object

Performs an HTTP PATCH request to the specified path (relative to self.instance_url). The body of the request is taken from data. Query parameters are included from parameters. The required Authorization header is automatically included, as are any additional headers specified in headers. Returns the HTTPResult if it is of type HTTPSuccess- raises SalesForceError otherwise.

Raises:



304
305
306
307
308
309
310
311
312
313
314
# File 'lib/databasedotcom/client.rb', line 304

def http_patch(path, data=nil, parameters={}, headers={})
  req = Net::HTTP.new(URI.parse(self.instance_url).host, 443)
  req.use_ssl = true
  path_parameters = (parameters || {}).collect { |k, v| "#{URI.escape(k.to_s)}=#{URI.escape(v.to_s)}" }.join('&')
  encoded_path = [URI.escape(path), path_parameters.empty? ? nil : path_parameters].compact.join('?')
  log_request(encoded_path, data)
  result = req.send_request("PATCH", encoded_path, data, {"Content-Type" => data ? "application/json" : "text/plain", "Authorization" => "OAuth #{self.oauth_token}"}.merge(headers))
  log_response(result)
  raise SalesForceError.new(result) unless result.is_a?(Net::HTTPSuccess)
  result
end

#http_post(path, data = nil, parameters = {}, headers = {}) ⇒ Object

Performs an HTTP POST request to the specified path (relative to self.instance_url). The body of the request is taken from data. Query parameters are included from parameters. The required Authorization header is automatically included, as are any additional headers specified in headers. Returns the HTTPResult if it is of type HTTPSuccess- raises SalesForceError otherwise.

Raises:



289
290
291
292
293
294
295
296
297
298
299
# File 'lib/databasedotcom/client.rb', line 289

def http_post(path, data=nil, parameters={}, headers={})
  req = Net::HTTP.new(URI.parse(self.instance_url).host, 443)
  req.use_ssl = true
  path_parameters = (parameters || {}).collect { |k, v| "#{URI.escape(k.to_s)}=#{URI.escape(v.to_s)}" }.join('&')
  encoded_path = [URI.escape(path), path_parameters.empty? ? nil : path_parameters].compact.join('?')
  log_request(encoded_path, data)
  result = req.post(encoded_path, data, {"Content-Type" => data ? "application/json" : "text/plain", "Authorization" => "OAuth #{self.oauth_token}"}.merge(headers))
  log_response(result)
  raise SalesForceError.new(result) unless result.is_a?(Net::HTTPSuccess)
  result
end

#list_sobjectsObject

Returns an Array of Strings listing the class names for every type of Sobject in the database. Raises SalesForceError if an error occurs.



118
119
120
121
122
123
124
125
# File 'lib/databasedotcom/client.rb', line 118

def list_sobjects
  result = http_get("/services/data/v#{self.version}/sobjects")
  if result.is_a?(Net::HTTPOK)
    JSON.parse(result.body)["sobjects"].collect { |sobject| sobject["name"] }
  elsif result.is_a?(Net::HTTPBadRequest)
    raise SalesForceError.new(result)
  end
end

#materialize(classnames) ⇒ Object

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.

client.materialize("Contact") #=> Contact
client.materialize(%w(Contact Company)) #=> [Contact, Company]

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.



133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
# File 'lib/databasedotcom/client.rb', line 133

def materialize(classnames)
  classes = (classnames.is_a?(Array) ? classnames : [classnames]).collect do |clazz|
    original_classname = clazz
    clazz = original_classname[0].capitalize + original_classname[1..-1]
    unless module_namespace.const_defined?(clazz)
      new_class = module_namespace.const_set(clazz, Class.new(Databasedotcom::Sobject::Sobject))
      new_class.client = self
      new_class.materialize(original_classname)
      new_class
    else
      module_namespace.const_get(clazz)
    end
  end

  classes.length == 1 ? classes.first : classes
end

#next_page(path) ⇒ Object

Used by Collection objects. Returns a Collection of Sobjects from the specified URL path that represents the next page of paginated results.



188
189
190
191
# File 'lib/databasedotcom/client.rb', line 188

def next_page(path)
  result = http_get(path)
  collection_from(result.body)
end

#previous_page(path) ⇒ Object

Used by Collection objects. Returns a Collection of Sobjects from the specified URL path that represents the previous page of paginated results.



194
195
196
197
# File 'lib/databasedotcom/client.rb', line 194

def previous_page(path)
  result = http_get(path)
  collection_from(result.body)
end

#query(soql_expr) ⇒ Object

Returns a Collection of Sobjects of the class specified in the soql_expr, which is a valid SOQL expression. The objects will only be populated with the values of attributes specified in the query.

client.query("SELECT Name FROM Account") #=> [#<Account @Id=nil, @Name="Foo", ...>, #<Account @Id=nil, @Name="Bar", ...> ...]


174
175
176
177
# File 'lib/databasedotcom/client.rb', line 174

def query(soql_expr)
  result = http_get("/services/data/v#{self.version}/query?q=#{soql_expr}")
  collection_from(result.body)
end

#recentObject

Returns a Collection of recently touched items. The Collection contains Sobject instances that are fully populated with their correct values.



243
244
245
246
# File 'lib/databasedotcom/client.rb', line 243

def recent
  result = http_get("/services/data/v#{self.version}/recent")
  collection_from(result.body)
end

#search(sosl_expr) ⇒ Object

Returns a Collection of Sobject instances form the results of the SOSL search.

client.search("FIND {bar}") #=> [#<Account @Name="foobar", ...>, #<Account @Name="barfoo", ...> ...]


182
183
184
185
# File 'lib/databasedotcom/client.rb', line 182

def search(sosl_expr)
  result = http_get("/services/data/v#{self.version}/search?q=#{sosl_expr}")
  collection_from(result.body)
end

Returns an array of trending topic names.



249
250
251
252
253
# File 'lib/databasedotcom/client.rb', line 249

def trending_topics
  result = http_get("/services/data/v#{self.version}/chatter/topics/trending")
  result = JSON.parse(result.body)
  result["topics"].collect { |topic| topic["name"] }
end

#update(class_or_classname, record_id, new_attrs) ⇒ Object

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

client.update("Car", "rid", {"Color" => "Red"})


218
219
220
221
222
# File 'lib/databasedotcom/client.rb', line 218

def update(class_or_classname, record_id, new_attrs)
  class_or_classname = find_or_materialize(class_or_classname)
  json_for_update = coerced_json(new_attrs, class_or_classname)
  http_patch("/services/data/v#{self.version}/sobjects/#{class_or_classname.sobject_name}/#{record_id}", json_for_update)
end

#upsert(class_or_classname, field, value, attrs) ⇒ Object

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. If not found, it will create a new record with attrs.

client.upsert(Car, "Color", "Blue", {"Year" => "2012"})


228
229
230
231
232
# File 'lib/databasedotcom/client.rb', line 228

def upsert(class_or_classname, field, value, attrs)
  clazz = find_or_materialize(class_or_classname)
  json_for_update = coerced_json(attrs, clazz)
  http_patch("/services/data/v#{self.version}/sobjects/#{clazz.sobject_name}/#{field}/#{value}", json_for_update)
end