class Databasedotcom::Client
Interface for operating the Force.com REST API
Attributes
The CA file configured for this instance, if any
The client id (aka “Consumer Key”) to use for OAuth2 authentication
The client secret (aka “Consumer Secret” to use for OAuth2 authentication)
If true, print API debugging information to stdout. Defaults to false.
The host to use for OAuth2 authentication. Defaults to login.salesforce.com
The base URL to the authenticated user’s SalesForce instance
The OAuth access token in use by the client
The SalesForce organization id for the authenticated user’s Salesforce instance
The SalesForce password
The OAuth refresh token in use by the client
A Module in which to materialize Sobject
classes. Defaults to the global module (Object)
The SalesForce user id of the authenticated user
The SalesForce username
The SSL verify mode configured for this instance, if any
The API version the client is using. Defaults to 23.0
Public Class Methods
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
# 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
Public Instance Methods
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
# 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
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">
# 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
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")
# 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
Returns a description of the Sobject
specified by class_name. The description includes all fields and their properties for the Sobject
.
# 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
Returns an Array of Hashes listing the properties for every type of Sobject in the database. Raises SalesForceError
if an error occurs.
# 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
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", ...>
# 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
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.
# 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
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.
# 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
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.
# 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
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.
# 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
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.
# 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
Returns an Array of Strings listing the class names for every type of Sobject in the database. Raises SalesForceError
if an error occurs.
# 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
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
.
# 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
Used by Collection
objects. Returns a Collection
of Sobjects from the specified URL path that represents the next page of paginated results.
# File lib/databasedotcom/client.rb, line 217 def next_page(path) result = http_get(path) collection_from(result.body) end
Used by Collection
objects. Returns a Collection
of Sobjects from the specified URL path that represents the previous page of paginated results.
# File lib/databasedotcom/client.rb, line 223 def previous_page(path) result = http_get(path) collection_from(result.body) end
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", ...> ...]
# 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
Returns a Collection
of recently touched items. The Collection
contains Sobject
instances that are fully populated with their correct values.
# File lib/databasedotcom/client.rb, line 272 def recent result = http_get("/services/data/v#{self.version}/recent") collection_from(result.body) end
Returns a Collection
of Sobject
instances form the results of the SOSL search.
client.search("FIND {bar}") #=> [#<Account @Name="foobar", ...>, #<Account @Name="barfoo", ...> ...]
# 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.
# 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
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"})
# 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
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"})
# 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
Private Instance Methods
# File lib/databasedotcom/client.rb, line 493 def coerced_json(attrs, clazz) if attrs.is_a?(Hash) coerced_attrs = {} attrs.keys.each do |key| case clazz.field_type(key.to_s) when "multipicklist" coerced_attrs[key] = (attrs[key] || []).join(';') when "datetime" begin attrs[key] = DateTime.parse(attrs[key]) if attrs[key].is_a?(String) 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") rescue nil end when "date" if attrs[key] coerced_attrs[key] = attrs[key].respond_to?(:strftime) ? attrs[key].strftime("%Y-%m-%d") : attrs[key] else coerced_attrs[key] = nil end else coerced_attrs[key] = attrs[key] end end coerced_attrs.to_json else attrs end end
# File lib/databasedotcom/client.rb, line 431 def collection_from(response) response = JSON.parse(response) collection_from_hash( response ) end
# File lib/databasedotcom/client.rb, line 463 def collection_from_hash(data) array_response = data.is_a?(Array) if array_response records = data.collect { |rec| self.find(rec["attributes"]["type"], rec["Id"]) } else records = data["records"].collect do |record| record_from_hash( record ) end end Databasedotcom::Collection.new(self, array_response ? records.length : data["totalSize"], array_response ? nil : data["nextRecordsUrl"]).concat(records) end
# File lib/databasedotcom/client.rb, line 548 def const_defined_in_module(mod, const) mod.method(:const_defined?).arity == 1 ? mod.const_defined?(const) : mod.const_defined?(const, false) end
# File lib/databasedotcom/client.rb, line 394 def encode_parameters(parameters={}) (parameters || {}).collect { |k, v| "#{uri_escape(k)}=#{uri_escape(v)}" }.join('&') end
# File lib/databasedotcom/client.rb, line 390 def encode_path_with_params(path, parameters={}) [URI.escape(path), encode_parameters(parameters)].reject{|el| el.empty?}.join('?') end
# File lib/databasedotcom/client.rb, line 348 def ensure_expected_response(expected_result_class) response = yield unless response.is_a?(expected_result_class || Net::HTTPSuccess) if response.is_a?(Net::HTTPUnauthorized) if self.refresh_token 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| response = https_request(self.host).post(encoded_path, nil) if response.is_a?(Net::HTTPOK) parse_auth_response(response.body) end response end elsif self.username && self.password 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| response = https_request(self.host).post(encoded_path, nil) if response.is_a?(Net::HTTPOK) parse_auth_response(response.body) end response end end if response.is_a?(Net::HTTPSuccess) response = yield end end raise SalesForceError.new(response) unless response.is_a?(expected_result_class || Net::HTTPSuccess) end response end
# File lib/databasedotcom/client.rb, line 411 def find_or_materialize(class_or_classname) if class_or_classname.is_a?(Class) clazz = class_or_classname else match = class_or_classname.match(/(?:(.+)::)?(\w+)$/) preceding_namespace = match[1] classname = match[2] raise ArgumentError if preceding_namespace && preceding_namespace != module_namespace.name clazz = module_namespace.const_get(classname.to_sym) rescue nil clazz ||= self.materialize(classname) end clazz end
# File lib/databasedotcom/client.rb, line 382 def https_request(host=nil) Net::HTTP.new(host || URI.parse(self.instance_url).host, 443).tap do |http| http.use_ssl = true http.ca_file = self.ca_file if self.ca_file http.verify_mode = self.verify_mode if self.verify_mode end end
# File lib/databasedotcom/client.rb, line 523 def key_from_label(label) label.gsub(' ', '_') end
# File lib/databasedotcom/client.rb, line 398 def log_request(path, options={}) base_url = options[:host] ? "https://#{options[:host]}" : self.instance_url puts "***** REQUEST: #{path.include?(':') ? path : URI.join(base_url, path)}#{options[:data] ? " => #{options[:data]}" : ''}" if self.debugging end
# File lib/databasedotcom/client.rb, line 407 def log_response(result) puts "***** RESPONSE: #{result.class.name} -> #{result.body}" if self.debugging end
# File lib/databasedotcom/client.rb, line 425 def module_namespace _module = self.sobject_module _module = _module.constantize if _module.is_a? String _module || Object end
# File lib/databasedotcom/client.rb, line 537 def parse_auth_response(body) json = JSON.parse(body) parse_user_id_and_org_id_from_identity_url(json["id"]) self.instance_url = json["instance_url"] self.oauth_token = json["access_token"] end
# File lib/databasedotcom/client.rb, line 531 def parse_user_id_and_org_id_from_identity_url(identity_url) m = identity_url.match(/\/id\/([^\/]+)\/([^\/]+)$/) @org_id = m[1] rescue nil @user_id = m[2] rescue nil end
# File lib/databasedotcom/client.rb, line 544 def query_org_id query("select id from Organization")[0]["Id"] end
Converts a Hash of object data into a concrete SObject
# File lib/databasedotcom/client.rb, line 437 def record_from_hash(data) attributes = data.delete('attributes') new_record = find_or_materialize(attributes["type"]).new data.each do |name, value| field = new_record.description['fields'].find do |field| key_from_label(field["label"]) == name || field["name"] == name || field["relationshipName"] == name end # Field not found if field == nil break end # If reference/lookup field data was fetched, recursively build the child record and apply if value.is_a?(Hash) and field['type'] == 'reference' and field["relationshipName"] relation = record_from_hash( value ) set_value( new_record, field["relationshipName"], relation, 'reference' ) # Apply the raw value for all other field types else set_value(new_record, field["name"], value, field["type"]) if field end end new_record end
# File lib/databasedotcom/client.rb, line 476 def set_value(record, attr, value, attr_type) value_to_set = value case attr_type when "datetime" value_to_set = DateTime.parse(value) rescue nil when "date" value_to_set = Date.parse(value) rescue nil when "multipicklist" value_to_set = value.split(";") rescue [] end record.send("#{attr}=", value_to_set) end
# File lib/databasedotcom/client.rb, line 403 def uri_escape(str) URI.escape(str.to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]")) end
# File lib/databasedotcom/client.rb, line 527 def user_and_pass?(options) (self.username && self.password) || (options && options[:username] && options[:password]) end
# File lib/databasedotcom/client.rb, line 333 def with_encoded_path_and_checked_response(path, parameters, options = {}) ensure_expected_response(options[:expected_result_class]) do with_logging(encode_path_with_params(path, parameters), options) do |encoded_path| yield(encoded_path) end end end
# File lib/databasedotcom/client.rb, line 341 def with_logging(encoded_path, options) log_request(encoded_path, options) response = yield encoded_path log_response(response) response end