databasedotcom_cloudfuji 1.3.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.
@@ -0,0 +1,45 @@
1
+ require 'databasedotcom/chatter/record'
2
+ require 'databasedotcom/chatter/photo_methods'
3
+
4
+ module Databasedotcom
5
+ module Chatter
6
+ # A group of Users
7
+ class Group < Record
8
+ include PhotoMethods
9
+
10
+ # Returns a Collection of GroupMembership instances for the Group identified by _group_id_.
11
+ def self.members(client, group_id)
12
+ url = "/services/data/v#{client.version}/chatter/groups/#{group_id}/members"
13
+ result = client.http_get(url)
14
+ response = JSON.parse(result.body)
15
+ collection = Databasedotcom::Collection.new(client, response["totalMemberCount"], response["nextPageUrl"], response["previousPageUrl"], response["currentPageUrl"])
16
+ response["members"].each do |member|
17
+ collection << GroupMembership.new(client, member)
18
+ end
19
+ collection
20
+ end
21
+
22
+ # Join the group identified by _group_id_ as the user identified by _user_id_.
23
+ def self.join(client, group_id, user_id="me")
24
+ url = "/services/data/v#{client.version}/chatter/groups/#{group_id}/members"
25
+ response = client.http_post(url, nil, :userId => user_id)
26
+ GroupMembership.new(client, response.body)
27
+ end
28
+
29
+ # Get a Collection of GroupMembership objects for this Group. Always makes a call to the server.
30
+ def members!
31
+ self.class.members(self.client, self.id)
32
+ end
33
+
34
+ # Get a Collection of GroupMembership objects for this Group. Returns cached data if it has been called before.
35
+ def members
36
+ @members ||= members!
37
+ end
38
+
39
+ # Join this Group as the user identified by _user_id_.
40
+ def join(user_id="me")
41
+ self.class.join(self.client, self.id, user_id)
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,9 @@
1
+ require 'databasedotcom/chatter/record'
2
+
3
+ module Databasedotcom
4
+ module Chatter
5
+ # A GroupMembership represents the membership of a certain User in a certain Group.
6
+ class GroupMembership < Record
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ require 'databasedotcom/chatter/record'
2
+
3
+ module Databasedotcom
4
+ module Chatter
5
+ # A like on a FeedItem
6
+ class Like < Record
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,29 @@
1
+ require 'databasedotcom/chatter/record'
2
+
3
+ module Databasedotcom
4
+ module Chatter
5
+ # A private message between two or more Users
6
+ class Message < Record
7
+
8
+ # Send a private message with the content _text_ to each user in the _recipients_ list.
9
+ def self.send_message(client, recipients, text)
10
+ url = "/services/data/v#{client.version}/chatter/users/me/messages"
11
+ recipients = recipients.is_a?(Array) ? recipients : [recipients]
12
+ response = client.http_post(url, nil, :text => text, :recipients => recipients.join(','))
13
+ Message.new(client, response.body)
14
+ end
15
+
16
+ # Send a reply to the message identified by _in_reply_to_message_id_ with content _text_.
17
+ def self.reply(client, in_reply_to_message_id, text)
18
+ url = "/services/data/v#{client.version}/chatter/users/me/messages"
19
+ response = client.http_post(url, nil, :text => text, :inReplyTo => in_reply_to_message_id)
20
+ Message.new(client, response.body)
21
+ end
22
+
23
+ # Send a reply to this Message with content _text_.
24
+ def reply(text)
25
+ self.class.reply(self.client, self.id, text)
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,55 @@
1
+ module Databasedotcom
2
+ module Chatter
3
+ # Defines methods for entities that can have photos i.e. Users, Groups.
4
+ module PhotoMethods
5
+ def self.included(base)
6
+ base.extend ClassMethods
7
+ end
8
+
9
+ # Defines class methods for resources that can have photos.
10
+ module ClassMethods
11
+ # Returns a Hash with urls for the small and large versions of the photo for a resource.
12
+ def photo(client, resource_id)
13
+ url = "/services/data/v#{client.version}/chatter/#{self.resource_name}/#{resource_id}/photo"
14
+ result = client.http_get(url)
15
+ JSON.parse(result.body)
16
+ end
17
+
18
+ # Uploads a photo for a resource with id _resource_id_.
19
+ #
20
+ # User.upload_photo(@client, "me", File.open("SomePicture.png"), "image/png")
21
+ def upload_photo(client, resource_id, io, file_type)
22
+ url = "/services/data/v#{client.version}/chatter/#{self.resource_name}/#{resource_id}/photo"
23
+ result = client.http_multipart_post(url, {"fileUpload" => UploadIO.new(io, file_type)})
24
+ JSON.parse(result.body)
25
+ end
26
+
27
+ # Deletes the photo for the resource with id _resource_id_.
28
+ def delete_photo(client, resource_id)
29
+ client.http_delete "/services/data/v#{client.version}/chatter/#{self.resource_name}/#{resource_id}/photo"
30
+ end
31
+ end
32
+
33
+ # Returns a Hash with urls for the small and large versions of the photo for this resource.
34
+ #
35
+ # User.find(@client, "me").photo #=> {"smallPhotoUrl"=>"/small/photo/url", "largePhotoUrl"=>"/large/photo/url"}
36
+ def photo
37
+ self.raw_hash["photo"]
38
+ end
39
+
40
+ # Uploads a photo for this resource.
41
+ #
42
+ # me = User.find(@client)
43
+ # me.upload_photo(File.open("SomePicture.png"), "image/png")
44
+ def upload_photo(io, file_type)
45
+ self.class.upload_photo(self.client, self.id, io, file_type)
46
+ end
47
+
48
+ # Deletes the photo for this resource.
49
+ def delete_photo
50
+ self.class.delete_photo(self.client, self.id)
51
+ photo
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,122 @@
1
+ require 'json'
2
+
3
+ module Databasedotcom
4
+ module Chatter
5
+ # Superclasses all Chatter resources except feeds. Some methods may not be supported by the Force.com API for certain subclasses.
6
+ class Record
7
+ attr_reader :raw_hash, :name, :id, :url, :type, :client
8
+
9
+ # Create a new record from the returned JSON response of an API request. Sets the client, name, id, url, and type attributes. Saves the raw response as +raw_hash+.
10
+ def initialize(client, response)
11
+ @client = client
12
+ @raw_hash = response.is_a?(Hash) ? response : JSON.parse(response)
13
+ @name = @raw_hash["name"]
14
+ @id = @raw_hash["id"]
15
+ @url = @raw_hash["url"]
16
+ @type = @raw_hash["type"]
17
+ end
18
+
19
+ # Find a single Record or a Collection of records by id. _resource_id_ can be a single id or a list of ids.
20
+ def self.find(client, resource_id, parameters={})
21
+ if resource_id.is_a?(Array)
22
+ resource_ids = resource_id.join(',')
23
+ url = "/services/data/v#{client.version}/chatter/#{self.resource_name}/batch/#{resource_ids}"
24
+ response = JSON.parse(client.http_get(url, parameters).body)
25
+ good_results = response["results"].select { |r| r["statusCode"] == 200 }
26
+ collection = Databasedotcom::Collection.new(client, good_results.length)
27
+ good_results.each do |result|
28
+ collection << self.new(client, result["result"])
29
+ end
30
+ collection
31
+ else
32
+ path_components = ["/services/data/v#{client.version}/chatter"]
33
+ if parameters.has_key?(:user_id)
34
+ path_components << "users/#{parameters[:user_id]}"
35
+ parameters.delete(:user_id)
36
+ end
37
+ path_components << "#{self.resource_name}/#{resource_id}"
38
+ url = path_components.join('/')
39
+ response = JSON.parse(client.http_get(url, parameters).body)
40
+ self.new(client, response)
41
+ end
42
+ end
43
+
44
+ # Return a Collection of records that match the _query_.
45
+ def self.search(client, query, parameters={})
46
+ self.all(client, parameters.merge(self.search_parameter_name => query))
47
+ end
48
+
49
+ # Return a Collection of all records.
50
+ def self.all(client, parameters={})
51
+ path_components = ["/services/data/v#{client.version}/chatter"]
52
+ if parameters.has_key?(:user_id)
53
+ path_components << "users/#{parameters[:user_id]}"
54
+ parameters.delete(:user_id)
55
+ end
56
+ path_components << self.resource_name
57
+ url = path_components.join('/')
58
+ result = client.http_get(url, parameters)
59
+ response = JSON.parse(result.body)
60
+ collection = Databasedotcom::Collection.new(client, self.total_size_of_collection(response), response["nextPageUrl"], response["previousPageUrl"], response["currentPageUrl"])
61
+ self.collection_from_response(response).each do |resource|
62
+ collection << self.new(client, resource)
63
+ end
64
+ collection
65
+ end
66
+
67
+ # Delete the Record identified by _resource_id_.
68
+ def self.delete(client, resource_id, parameters={})
69
+ path_components = ["/services/data/v#{client.version}/chatter"]
70
+ if parameters.has_key?(:user_id)
71
+ path_components << "users/#{parameters[:user_id]}"
72
+ parameters.delete(:user_id)
73
+ end
74
+ path_components << self.resource_name
75
+ path_components << resource_id
76
+ path = path_components.join('/')
77
+ client.http_delete(path, parameters)
78
+ end
79
+
80
+ # A Hash representation of the User that created this Record.
81
+ def user
82
+ self.raw_hash["user"]
83
+ end
84
+
85
+ # A Hash representation of the entity that is the parent of this Record.
86
+ def parent
87
+ self.raw_hash["parent"]
88
+ end
89
+
90
+ # Delete this record.
91
+ def delete(parameters={})
92
+ self.class.delete(self.client, self.id, parameters)
93
+ end
94
+
95
+ # Reload this record.
96
+ def reload
97
+ self.class.find(self.client, self.id)
98
+ end
99
+
100
+ # The REST resource name of this Record.
101
+ #
102
+ # GroupMembership.resource_name #=> group-memberships
103
+ def self.resource_name
104
+ (self.name.split('::').last).resourcerize + "s"
105
+ end
106
+
107
+ protected
108
+
109
+ def self.total_size_of_collection(response)
110
+ response["total"] || response["totalMemberCount"]
111
+ end
112
+
113
+ def self.collection_from_response(response)
114
+ response[self.resource_name]
115
+ end
116
+
117
+ def self.search_parameter_name
118
+ :q
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,9 @@
1
+ require 'databasedotcom/chatter/record'
2
+
3
+ module Databasedotcom
4
+ module Chatter
5
+ # A representation of a user "following" some other entity.
6
+ class Subscription < Record
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,153 @@
1
+ require 'databasedotcom/chatter/record'
2
+ require 'databasedotcom/chatter/photo_methods'
3
+
4
+ module Databasedotcom
5
+ module Chatter
6
+ # Defines a User in your org.
7
+ class User < Record
8
+ include PhotoMethods
9
+
10
+ # Returns a Collection of Subscription objects that represents all followers of the User identified by _subject_id_.
11
+ def self.followers(client, subject_id="me")
12
+ url = "/services/data/v#{client.version}/chatter/users/#{subject_id}/followers"
13
+ result = client.http_get(url)
14
+ response = JSON.parse(result.body)
15
+ collection = Databasedotcom::Collection.new(client, response["total"], response["nextPageUrl"], response["previousPageUrl"], response["currentPageUrl"])
16
+ response["followers"].each do |subscription|
17
+ collection << Subscription.new(client, subscription)
18
+ end
19
+ collection
20
+ end
21
+
22
+ # Returns a Collection of Subscription objects that represent all entities that the User identified by _subject_id_ is following.
23
+ def self.following(client, subject_id="me")
24
+ url = "/services/data/v#{client.version}/chatter/users/#{subject_id}/following"
25
+ result = client.http_get(url)
26
+ response = JSON.parse(result.body)
27
+ collection = Databasedotcom::Collection.new(client, response["total"], response["nextPageUrl"], response["previousPageUrl"], response["currentPageUrl"])
28
+ response["following"].each do |subscription|
29
+ collection << Subscription.new(client, subscription)
30
+ end
31
+ collection
32
+ end
33
+
34
+ # Returns a Collection of Group objects that represent all the groups that the User identified by _subject_id_ is a part of.
35
+ def self.groups(client, subject_id="me")
36
+ url = "/services/data/v#{client.version}/chatter/users/#{subject_id}/groups"
37
+ result = client.http_get(url)
38
+ response = JSON.parse(result.body)
39
+ collection = Databasedotcom::Collection.new(client, response["total"], response["nextPageUrl"], response["previousPageUrl"], response["currentPageUrl"])
40
+ response["groups"].each do |group|
41
+ collection << Group.new(client, group)
42
+ end
43
+ collection
44
+ end
45
+
46
+ # Returns the current status of the User identified by _subject_id_.
47
+ def self.status(client, subject_id="me")
48
+ url = "/services/data/v#{client.version}/chatter/users/#{subject_id}/status"
49
+ result = client.http_get(url)
50
+ JSON.parse(result.body)
51
+ end
52
+
53
+ # Posts a status update as the User identified by _subject_id_ with content _text_.
54
+ def self.post_status(client, subject_id, text)
55
+ url = "/services/data/v#{client.version}/chatter/users/#{subject_id}/status"
56
+ result = client.http_post(url, nil, :text => text)
57
+ JSON.parse(result.body)
58
+ end
59
+
60
+ # Deletes the status of User identified by _subject_id_.
61
+ def self.delete_status(client, subject_id="me")
62
+ client.http_delete "/services/data/v#{client.version}/chatter/users/#{subject_id}/status"
63
+ end
64
+
65
+ # Creates and returns a new Subscription object that represents the User identified by _subject_id_ following the resource identified by _resource_id_.
66
+ def self.follow(client, subject_id, resource_id)
67
+ response = client.http_post("/services/data/v#{client.version}/chatter/users/#{subject_id}/following", nil, :subjectId => resource_id)
68
+ Subscription.new(client, response.body)
69
+ end
70
+
71
+ # Returns a Collection of conversations that belong to the User identified by _subject_id_.
72
+ def self.conversations(client, subject_id)
73
+ Conversation.all(client, :user_id => subject_id)
74
+ end
75
+
76
+ # Returns a Collection of private messages that belong to the User identified by _subject_id_.
77
+ def self.messages(client, subject_id)
78
+ Message.all(client, :user_id => subject_id)
79
+ end
80
+
81
+ # Get a Collection of Subscription objects for this User. Always makes a call to the server.
82
+ def followers!
83
+ self.class.followers(self.client, self.id)
84
+ end
85
+
86
+ # Get a Collection of Subscription objects for this User. Returns cached data if it has been called before.
87
+ def followers
88
+ @followers ||= followers!
89
+ end
90
+
91
+ # Get a Collection of Subscription objects that represents all resources that this User is following. Always makes a call to the server.
92
+ def following!
93
+ self.class.following(self.client, self.id)
94
+ end
95
+
96
+ # Get a Collection of Subscription objects that represents all resources that this User is following. Returns cached data if it has been called before.
97
+ def following
98
+ @following ||= following!
99
+ end
100
+
101
+ # Returns this current status of this User.
102
+ def status
103
+ self.raw_hash["currentStatus"]
104
+ end
105
+
106
+ # Posts a new status with content _text_ for this User.
107
+ def post_status(text)
108
+ self.class.post_status(self.client, self.id, text)
109
+ end
110
+
111
+ # Deletes the current status of this User. Returns the deleted status.
112
+ def delete_status
113
+ self.class.delete_status(self.client, self.id)
114
+ status
115
+ end
116
+
117
+ # Get a Collection of Group objects that represents all groups that this User is in. Always makes a call to the server.
118
+ def groups!
119
+ self.class.groups(self.client, self.id)
120
+ end
121
+
122
+ # Get a Collection of Group objects that represents all groups that this User is in. Returns cached data if it has been called before.
123
+ def groups
124
+ @groups ||= groups!
125
+ end
126
+
127
+ # Creates a new Subscription that represents this User following the resource with id _record_id_.
128
+ def follow(record_id)
129
+ self.class.follow(self.client, self.id, record_id)
130
+ end
131
+
132
+ # Get a Collection of Conversation objects that represents the conversations for this User. Always makes a call to the server.
133
+ def conversations!
134
+ self.class.conversations(self.client, self.id)
135
+ end
136
+
137
+ # Get a Collection of Conversation objects that represents the conversations for this User. Returns cached data if it has been called before.
138
+ def conversations
139
+ @conversations ||= conversations!
140
+ end
141
+
142
+ # Get a Collection of Message objects that represents the messages for this User. Always makes a call to the server.
143
+ def messages!
144
+ self.class.messages(self.client, self.id)
145
+ end
146
+
147
+ # Get a Collection of Message objects that represents the messages for this User. Returns cached data if it has been called before.
148
+ def messages
149
+ @messages ||= messages!
150
+ end
151
+ end
152
+ end
153
+ end
@@ -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