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
    ca_file: some/ca/file.cert
    verify_mode: OpenSSL::SSL::VERIFY_PEER
    
  • A Hash containing the following keys:

    client_id
    client_secret
    host
    debugging
    version
    sobject_module
    ca_file
    verify_mode
    

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


64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
# File 'lib/databasedotcom/client.rb', line 64

def initialize(options = {})
  if options.is_a?(String)
    @options = YAML.load_file(options)
    @options["verify_mode"] = @options["verify_mode"].constantize if @options["verify_mode"] && @options["verify_mode"].is_a?(String)
  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]
  self.ca_file = ENV['DATABASEDOTCOM_CA_FILE'] || @options[:ca_file]
  self.verify_mode = ENV['DATABASEDOTCOM_VERIFY_MODE'] || @options[:verify_mode]
  self.verify_mode = self.verify_mode.to_i if self.verify_mode
end

Instance Attribute Details

#ca_fileObject

The CA file configured for this instance, if any


36
37
38
# File 'lib/databasedotcom/client.rb', line 36

def ca_file
  @ca_file
end

#client_idObject

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


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

def client_id
  @client_id
end

#client_secretObject

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


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

def client_secret
  @client_secret
end

#debuggingObject

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


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

def debugging
  @debugging
end

#hostObject

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


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

def host
  @host
end

#instance_urlObject

The base URL to the authenticated user's SalesForce instance


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

def instance_url
  @instance_url
end

#oauth_tokenObject

The OAuth access token in use by the client


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

def oauth_token
  @oauth_token
end

#org_idObject (readonly)

The SalesForce organization id for the authenticated user's Salesforce instance


34
35
36
# File 'lib/databasedotcom/client.rb', line 34

def org_id
  @org_id
end

#passwordObject

The SalesForce password


32
33
34
# File 'lib/databasedotcom/client.rb', line 32

def password
  @password
end

#refresh_tokenObject

The OAuth refresh token in use by the client


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

def refresh_token
  @refresh_token
end

#sobject_moduleObject

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


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

def sobject_module
  @sobject_module
end

#user_idObject (readonly)

The SalesForce user id of the authenticated user


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

def user_id
  @user_id
end

#usernameObject

The SalesForce username


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

def username
  @username
end

#verify_modeObject

The SSL verify mode configured for this instance, if any


38
39
40
# File 'lib/databasedotcom/client.rb', line 38

def verify_mode
  @verify_mode
end

#versionObject

The API version the client is using. Defaults to 23.0


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

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. options may also optionally contain the key :refresh_token

Raises SalesForceError if an error occurs


103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
# File 'lib/databasedotcom/client.rb', line 103

def authenticate(options = nil)
  if user_and_pass?(options)
    req = https_request(self.host)
    user = self.username || options[:username]
    pass = self.password || options[:password]
    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)
    log_request("https://#{self.host}/#{path}")
    result = req.post(path, "")
    log_response(result)
    raise SalesForceError.new(result) unless result.is_a?(Net::HTTPOK)
    self.username = user
    self.password = pass
    parse_auth_response(result.body)
  elsif options.is_a?(Hash)
    if options.has_key?("provider")
      parse_user_id_and_org_id_from_identity_url(options["uid"])
      self.instance_url = options["credentials"]["instance_url"]
      self.oauth_token = options["credentials"]["token"]
      self.refresh_token = options["credentials"]["refresh_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]
      self.refresh_token = options[:refresh_token]
    end
  end

  self.version = "22.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">

231
232
233
234
235
236
237
238
239
240
241
242
# File 'lib/databasedotcom/client.rb', line 231

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")

266
267
268
269
# File 'lib/databasedotcom/client.rb', line 266

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.


180
181
182
183
# File 'lib/databasedotcom/client.rb', line 180

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

#describe_sobjectsObject

Returns an Array of Hashes listing the properties for every type of Sobject in the database. Raises SalesForceError if an error occurs.


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

def describe_sobjects
  result = http_get("/services/data/v#{self.version}/sobjects")
  JSON.parse(result.body)["sobjects"]
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", ...>

189
190
191
192
193
194
195
196
197
198
# File 'lib/databasedotcom/client.rb', line 189

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.


297
298
299
300
301
# File 'lib/databasedotcom/client.rb', line 297

def http_delete(path, parameters={}, headers={})
  with_encoded_path_and_checked_response(path, parameters, {:expected_result_class => Net::HTTPNoContent}) do |encoded_path|
    https_request.delete(encoded_path, {"Authorization" => "OAuth #{self.oauth_token}"}.merge(headers))
  end
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.


287
288
289
290
291
# File 'lib/databasedotcom/client.rb', line 287

def http_get(path, parameters={}, headers={})
  with_encoded_path_and_checked_response(path, parameters) do |encoded_path|
    https_request.get(encoded_path, {"Authorization" => "OAuth #{self.oauth_token}"}.merge(headers))
  end
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.


325
326
327
328
329
# File 'lib/databasedotcom/client.rb', line 325

def http_multipart_post(path, parts, parameters={}, headers={})
  with_encoded_path_and_checked_response(path, parameters) do |encoded_path|
    https_request.request(Net::HTTP::Post::Multipart.new(encoded_path, parts, {"Authorization" => "OAuth #{self.oauth_token}"}.merge(headers)))
  end
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.


315
316
317
318
319
# File 'lib/databasedotcom/client.rb', line 315

def http_patch(path, data=nil, parameters={}, headers={})
  with_encoded_path_and_checked_response(path, parameters, {:data => data}) do |encoded_path|
    https_request.send_request("PATCH", encoded_path, data, {"Content-Type" => data ? "application/json" : "text/plain", "Authorization" => "OAuth #{self.oauth_token}"}.merge(headers))
  end
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.


306
307
308
309
310
# File 'lib/databasedotcom/client.rb', line 306

def http_post(path, data=nil, parameters={}, headers={})
  with_encoded_path_and_checked_response(path, parameters, {:data => data}) do |encoded_path|
    https_request.post(encoded_path, data, {"Content-Type" => data ? "application/json" : "text/plain", "Authorization" => "OAuth #{self.oauth_token}"}.merge(headers))
  end
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.


141
142
143
144
145
146
147
148
# File 'lib/databasedotcom/client.rb', line 141

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.


156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
# File 'lib/databasedotcom/client.rb', line 156

def materialize(classnames)
  classes = (classnames.is_a?(Array) ? classnames : [classnames]).collect do |clazz|
    original_classname = clazz
    clazz = original_classname[0,1].capitalize + original_classname[1..-1]
    unless const_defined_in_module(module_namespace, 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.


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

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.


223
224
225
226
# File 'lib/databasedotcom/client.rb', line 223

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", ...> ...]

203
204
205
206
# File 'lib/databasedotcom/client.rb', line 203

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.


272
273
274
275
# File 'lib/databasedotcom/client.rb', line 272

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", ...> ...]

211
212
213
214
# File 'lib/databasedotcom/client.rb', line 211

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.


278
279
280
281
282
# File 'lib/databasedotcom/client.rb', line 278

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"})

247
248
249
250
251
# File 'lib/databasedotcom/client.rb', line 247

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"})

257
258
259
260
261
# File 'lib/databasedotcom/client.rb', line 257

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