databasedotcom 1.0.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.
- data/MIT-LICENSE +20 -0
- data/README.rdoc +154 -0
- data/lib/databasedotcom.rb +7 -0
- data/lib/databasedotcom/chatter.rb +11 -0
- data/lib/databasedotcom/chatter/comment.rb +10 -0
- data/lib/databasedotcom/chatter/conversation.rb +100 -0
- data/lib/databasedotcom/chatter/feed.rb +64 -0
- data/lib/databasedotcom/chatter/feed_item.rb +40 -0
- data/lib/databasedotcom/chatter/feeds.rb +5 -0
- data/lib/databasedotcom/chatter/filter_feed.rb +14 -0
- data/lib/databasedotcom/chatter/group.rb +45 -0
- data/lib/databasedotcom/chatter/group_membership.rb +9 -0
- data/lib/databasedotcom/chatter/like.rb +9 -0
- data/lib/databasedotcom/chatter/message.rb +29 -0
- data/lib/databasedotcom/chatter/photo_methods.rb +55 -0
- data/lib/databasedotcom/chatter/record.rb +122 -0
- data/lib/databasedotcom/chatter/subscription.rb +9 -0
- data/lib/databasedotcom/chatter/user.rb +153 -0
- data/lib/databasedotcom/client.rb +421 -0
- data/lib/databasedotcom/collection.rb +37 -0
- data/lib/databasedotcom/core_extensions.rb +3 -0
- data/lib/databasedotcom/core_extensions/class_extensions.rb +41 -0
- data/lib/databasedotcom/core_extensions/hash_extensions.rb +8 -0
- data/lib/databasedotcom/core_extensions/string_extensions.rb +8 -0
- data/lib/databasedotcom/sales_force_error.rb +26 -0
- data/lib/databasedotcom/sobject.rb +2 -0
- data/lib/databasedotcom/sobject/sobject.rb +291 -0
- data/lib/databasedotcom/version.rb +3 -0
- metadata +138 -0
@@ -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,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,421 @@
|
|
1
|
+
require 'net/https'
|
2
|
+
require 'json'
|
3
|
+
require 'net/http/post/multipart'
|
4
|
+
|
5
|
+
module Databasedotcom
|
6
|
+
# Interface for operating the Force.com REST API
|
7
|
+
class Client
|
8
|
+
# The client id (aka "Consumer Key") to use for OAuth2 authentication
|
9
|
+
attr_accessor :client_id
|
10
|
+
# The client secret (aka "Consumer Secret" to use for OAuth2 authentication)
|
11
|
+
attr_accessor :client_secret
|
12
|
+
# The OAuth access token in use by the client
|
13
|
+
attr_accessor :oauth_token
|
14
|
+
# The base URL to the authenticated user's SalesForce instance
|
15
|
+
attr_accessor :instance_url
|
16
|
+
# If true, print API debugging information to stdout. Defaults to false.
|
17
|
+
attr_accessor :debugging
|
18
|
+
# The host to use for OAuth2 authentication. Defaults to +login.salesforce.com+
|
19
|
+
attr_accessor :host
|
20
|
+
# The API version the client is using. Defaults to 23.0
|
21
|
+
attr_accessor :version
|
22
|
+
# A Module in which to materialize Sobject classes. Defaults to the global module (Object)
|
23
|
+
attr_accessor :sobject_module
|
24
|
+
# The SalesForce user id of the authenticated user
|
25
|
+
attr_reader :user_id
|
26
|
+
# The SalesForce username
|
27
|
+
attr_accessor :username
|
28
|
+
# The SalesForce password
|
29
|
+
attr_accessor :password
|
30
|
+
|
31
|
+
# Returns a new client object. _options_ can be one of the following
|
32
|
+
#
|
33
|
+
# * A String containing the name of a YAML file formatted like:
|
34
|
+
# ---
|
35
|
+
# client_id: <your_salesforce_client_id>
|
36
|
+
# client_secret: <your_salesforce_client_secret>
|
37
|
+
# host: login.salesforce.com
|
38
|
+
# debugging: true
|
39
|
+
# version: 23.0
|
40
|
+
# sobject_module: My::Module
|
41
|
+
# * A Hash containing the following keys:
|
42
|
+
# client_id
|
43
|
+
# client_secret
|
44
|
+
# host
|
45
|
+
# debugging
|
46
|
+
# version
|
47
|
+
# sobject_module
|
48
|
+
# If the environment variables DATABASEDOTCOM_CLIENT_ID, DATABASEDOTCOM_CLIENT_SECRET, DATABASEDOTCOM_HOST,
|
49
|
+
# DATABASEDOTCOM_DEBUGGING, DATABASEDOTCOM_VERSION, and/or DATABASEDOTCOM_SOBJECT_MODULE are present, they
|
50
|
+
# override any other values provided
|
51
|
+
def initialize(options = {})
|
52
|
+
if options.is_a?(String)
|
53
|
+
@options = YAML.load_file(options)
|
54
|
+
else
|
55
|
+
@options = options
|
56
|
+
end
|
57
|
+
@options.symbolize_keys!
|
58
|
+
|
59
|
+
if ENV['DATABASE_COM_URL']
|
60
|
+
url = URI.parse(ENV['DATABASE_COM_URL'])
|
61
|
+
url_options = Hash[url.query.split("&").map{|q| q.split("=")}].symbolize_keys!
|
62
|
+
self.host = url.host
|
63
|
+
self.client_id = url_options[:oauth_key]
|
64
|
+
self.client_secret = url_options[:oauth_secret]
|
65
|
+
self.username = url_options[:user]
|
66
|
+
self.password = url_options[:password]
|
67
|
+
self.sobject_module = "Databasedotcom::Sobject"
|
68
|
+
else
|
69
|
+
self.client_id = ENV['DATABASEDOTCOM_CLIENT_ID'] || @options[:client_id]
|
70
|
+
self.client_secret = ENV['DATABASEDOTCOM_CLIENT_SECRET'] || @options[:client_secret]
|
71
|
+
self.host = ENV['DATABASEDOTCOM_HOST'] || @options[:host] || "login.salesforce.com"
|
72
|
+
self.debugging = ENV['DATABASEDOTCOM_DEBUGGING'] || @options[:debugging]
|
73
|
+
self.version = ENV['DATABASEDOTCOM_VERSION'] || @options[:version]
|
74
|
+
self.version = self.version.to_s if self.version
|
75
|
+
self.sobject_module = ENV['DATABASEDOTCOM_SOBJECT_MODULE'] || (@options && @options[:sobject_module])
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Authenticate to the Force.com API. _options_ is a Hash, interpreted as follows:
|
80
|
+
#
|
81
|
+
# * 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
|
82
|
+
# * 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
|
83
|
+
# * 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
|
84
|
+
#
|
85
|
+
# Raises SalesForceError if an error occurs
|
86
|
+
def authenticate(options = nil)
|
87
|
+
if (options[:username] && options[:password])
|
88
|
+
req = Net::HTTP.new(self.host, 443)
|
89
|
+
req.use_ssl=true
|
90
|
+
path = "/services/oauth2/token?grant_type=password&client_id=#{self.client_id}&client_secret=#{client_secret}&username=#{options[:username]}&password=#{options[:password]}"
|
91
|
+
log_request("https://#{self.host}/#{path}")
|
92
|
+
result = req.post(path, "")
|
93
|
+
log_response(result)
|
94
|
+
raise SalesForceError.new(result) unless result.is_a?(Net::HTTPOK)
|
95
|
+
json = JSON.parse(result.body)
|
96
|
+
@user_id = json["id"].match(/\/([^\/]+)$/)[1] rescue nil
|
97
|
+
self.instance_url = json["instance_url"]
|
98
|
+
self.oauth_token = json["access_token"]
|
99
|
+
elsif options.is_a?(Hash)
|
100
|
+
if options.has_key?("provider")
|
101
|
+
@user_id = options["extra"]["user_hash"]["user_id"] rescue nil
|
102
|
+
self.instance_url = options["credentials"]["instance_url"]
|
103
|
+
self.oauth_token = options["credentials"]["token"]
|
104
|
+
else
|
105
|
+
raise ArgumentError unless options.has_key?(:token) && options.has_key?(:instance_url)
|
106
|
+
self.instance_url = options[:instance_url]
|
107
|
+
self.oauth_token = options[:token]
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
self.version = "23.0" unless self.version
|
112
|
+
|
113
|
+
self.oauth_token
|
114
|
+
end
|
115
|
+
|
116
|
+
# Returns an Array of Strings listing the class names for every type of _Sobject_ in the database. Raises SalesForceError if an error occurs.
|
117
|
+
def list_sobjects
|
118
|
+
result = http_get("/services/data/v#{self.version}/sobjects")
|
119
|
+
if result.is_a?(Net::HTTPOK)
|
120
|
+
JSON.parse(result.body)["sobjects"].collect { |sobject| sobject["name"] }
|
121
|
+
elsif result.is_a?(Net::HTTPBadRequest)
|
122
|
+
raise SalesForceError.new(result)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# 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.
|
127
|
+
#
|
128
|
+
# client.materialize("Contact") #=> Contact
|
129
|
+
# client.materialize(%w(Contact Company)) #=> [Contact, Company]
|
130
|
+
#
|
131
|
+
# 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.
|
132
|
+
def materialize(classnames)
|
133
|
+
classes = (classnames.is_a?(Array) ? classnames : [classnames]).collect do |clazz|
|
134
|
+
original_classname = clazz
|
135
|
+
clazz = original_classname[0].capitalize + original_classname[1..-1]
|
136
|
+
unless module_namespace.const_defined?(clazz)
|
137
|
+
new_class = module_namespace.const_set(clazz, Class.new(Databasedotcom::Sobject::Sobject))
|
138
|
+
new_class.client = self
|
139
|
+
new_class.materialize(original_classname)
|
140
|
+
new_class
|
141
|
+
else
|
142
|
+
module_namespace.const_get(clazz)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
classes.length == 1 ? classes.first : classes
|
147
|
+
end
|
148
|
+
|
149
|
+
# Returns a description of the Sobject specified by _class_name_. The description includes all fields and their properties for the Sobject.
|
150
|
+
def describe_sobject(class_name)
|
151
|
+
result = http_get("/services/data/v#{self.version}/sobjects/#{class_name}/describe")
|
152
|
+
JSON.parse(result.body)
|
153
|
+
end
|
154
|
+
|
155
|
+
# 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_.
|
156
|
+
# If given a Class that is not defined, it will attempt to materialize the class on demand.
|
157
|
+
#
|
158
|
+
# client.find(Account, "recordid") #=> #<Account @Id="recordid", ...>
|
159
|
+
def find(class_or_classname, record_id)
|
160
|
+
class_or_classname = find_or_materialize(class_or_classname)
|
161
|
+
result = http_get("/services/data/v#{self.version}/sobjects/#{class_or_classname.sobject_name}/#{record_id}")
|
162
|
+
response = JSON.parse(result.body)
|
163
|
+
new_record = class_or_classname.new
|
164
|
+
class_or_classname.description["fields"].each do |field|
|
165
|
+
set_value(new_record, field["name"], response[key_from_label(field["label"])] || response[field["name"]], field["type"])
|
166
|
+
end
|
167
|
+
new_record
|
168
|
+
end
|
169
|
+
|
170
|
+
# 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.
|
171
|
+
#
|
172
|
+
# client.query("SELECT Name FROM Account") #=> [#<Account @Id=nil, @Name="Foo", ...>, #<Account @Id=nil, @Name="Bar", ...> ...]
|
173
|
+
def query(soql_expr)
|
174
|
+
result = http_get("/services/data/v#{self.version}/query?q=#{soql_expr}")
|
175
|
+
collection_from(result.body)
|
176
|
+
end
|
177
|
+
|
178
|
+
# 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.
|
179
|
+
#
|
180
|
+
# client.search("FIND {bar}") #=> [#<Account @Name="foobar", ...>, #<Account @Name="barfoo", ...> ...]
|
181
|
+
def search(sosl_expr)
|
182
|
+
result = http_get("/services/data/v#{self.version}/search?q=#{sosl_expr}")
|
183
|
+
collection_from(result.body)
|
184
|
+
end
|
185
|
+
|
186
|
+
# Used by Collection objects. Returns a Collection of Sobjects from the specified URL path that represents the next page of paginated results.
|
187
|
+
def next_page(path)
|
188
|
+
result = http_get(path)
|
189
|
+
collection_from(result.body)
|
190
|
+
end
|
191
|
+
|
192
|
+
# Used by Collection objects. Returns a Collection of Sobjects from the specified URL path that represents the previous page of paginated results.
|
193
|
+
def previous_page(path)
|
194
|
+
result = http_get(path)
|
195
|
+
collection_from(result.body)
|
196
|
+
end
|
197
|
+
|
198
|
+
# Returns a new instance of _class_or_classname_ (which can be passed in as either a String or a Class) with the specified attributes.
|
199
|
+
#
|
200
|
+
# client.create("Car", {"Color" => "Blue", "Year" => "2011"}) #=> #<Car @Id="recordid", @Color="Blue", @Year="2011">
|
201
|
+
def create(class_or_classname, object_attrs)
|
202
|
+
class_or_classname = find_or_materialize(class_or_classname)
|
203
|
+
json_for_assignment = coerced_json(object_attrs, class_or_classname)
|
204
|
+
result = http_post("/services/data/v#{self.version}/sobjects/#{class_or_classname.sobject_name}", json_for_assignment)
|
205
|
+
new_object = class_or_classname.new
|
206
|
+
JSON.parse(json_for_assignment).each do |property, value|
|
207
|
+
set_value(new_object, property, value, class_or_classname.type_map[property][:type])
|
208
|
+
end
|
209
|
+
id = JSON.parse(result.body)["id"]
|
210
|
+
set_value(new_object, "Id", id, "id")
|
211
|
+
new_object
|
212
|
+
end
|
213
|
+
|
214
|
+
# 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
|
215
|
+
#
|
216
|
+
# client.update("Car", "rid", {"Color" => "Red"})
|
217
|
+
def update(class_or_classname, record_id, new_attrs)
|
218
|
+
class_or_classname = find_or_materialize(class_or_classname)
|
219
|
+
json_for_update = coerced_json(new_attrs, class_or_classname)
|
220
|
+
http_patch("/services/data/v#{self.version}/sobjects/#{class_or_classname.sobject_name}/#{record_id}", json_for_update)
|
221
|
+
end
|
222
|
+
|
223
|
+
# 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.
|
224
|
+
# If not found, it will create a new record with _attrs_.
|
225
|
+
#
|
226
|
+
# client.upsert(Car, "Color", "Blue", {"Year" => "2012"})
|
227
|
+
def upsert(class_or_classname, field, value, attrs)
|
228
|
+
clazz = find_or_materialize(class_or_classname)
|
229
|
+
json_for_update = coerced_json(attrs, clazz)
|
230
|
+
http_patch("/services/data/v#{self.version}/sobjects/#{clazz.sobject_name}/#{field}/#{value}", json_for_update)
|
231
|
+
end
|
232
|
+
|
233
|
+
# Deletes the record of type _class_or_classname_ with id of _record_id_. _class_or_classname_ can be a String or a Class.
|
234
|
+
#
|
235
|
+
# client.delete(Car, "rid")
|
236
|
+
def delete(class_or_classname, record_id)
|
237
|
+
clazz = find_or_materialize(class_or_classname)
|
238
|
+
http_delete("/services/data/v#{self.version}/sobjects/#{clazz.sobject_name}/#{record_id}")
|
239
|
+
end
|
240
|
+
|
241
|
+
# Returns a Collection of recently touched items. The Collection contains Sobject instances that are fully populated with their correct values.
|
242
|
+
def recent
|
243
|
+
result = http_get("/services/data/v#{self.version}/recent")
|
244
|
+
collection_from(result.body)
|
245
|
+
end
|
246
|
+
|
247
|
+
# Returns an array of trending topic names.
|
248
|
+
def trending_topics
|
249
|
+
result = http_get("/services/data/v#{self.version}/chatter/topics/trending")
|
250
|
+
result = JSON.parse(result.body)
|
251
|
+
result["topics"].collect { |topic| topic["name"] }
|
252
|
+
end
|
253
|
+
|
254
|
+
# Performs an HTTP GET request to the specified path (relative to self.instance_url). Query parameters are included from _parameters_. The required
|
255
|
+
# +Authorization+ header is automatically included, as are any additional headers specified in _headers_. Returns the HTTPResult if it is of type
|
256
|
+
# HTTPSuccess- raises SalesForceError otherwise.
|
257
|
+
def http_get(path, parameters={}, headers={})
|
258
|
+
req = Net::HTTP.new(URI.parse(self.instance_url).host, 443)
|
259
|
+
req.use_ssl = true
|
260
|
+
path_parameters = (parameters || {}).collect { |k, v| "#{URI.escape(k.to_s)}=#{URI.escape(v.to_s)}" }.join('&')
|
261
|
+
encoded_path = [URI.escape(path), path_parameters.empty? ? nil : path_parameters].compact.join('?')
|
262
|
+
log_request(encoded_path)
|
263
|
+
result = req.get(encoded_path, {"Authorization" => "OAuth #{self.oauth_token}"}.merge(headers))
|
264
|
+
log_response(result)
|
265
|
+
raise SalesForceError.new(result) unless result.is_a?(Net::HTTPSuccess)
|
266
|
+
result
|
267
|
+
end
|
268
|
+
|
269
|
+
|
270
|
+
# Performs an HTTP DELETE request to the specified path (relative to self.instance_url). Query parameters are included from _parameters_. The required
|
271
|
+
# +Authorization+ header is automatically included, as are any additional headers specified in _headers_. Returns the HTTPResult if it is of type
|
272
|
+
# HTTPSuccess- raises SalesForceError otherwise.
|
273
|
+
def http_delete(path, parameters={}, headers={})
|
274
|
+
req = Net::HTTP.new(URI.parse(self.instance_url).host, 443)
|
275
|
+
req.use_ssl = true
|
276
|
+
path_parameters = (parameters || {}).collect { |k, v| "#{URI.escape(k.to_s)}=#{URI.escape(v.to_s)}" }.join('&')
|
277
|
+
encoded_path = [URI.escape(path), path_parameters.empty? ? nil : path_parameters].compact.join('?')
|
278
|
+
log_request(encoded_path)
|
279
|
+
result = req.delete(encoded_path, {"Authorization" => "OAuth #{self.oauth_token}"}.merge(headers))
|
280
|
+
log_response(result)
|
281
|
+
raise SalesForceError.new(result) unless result.is_a?(Net::HTTPNoContent)
|
282
|
+
result
|
283
|
+
end
|
284
|
+
|
285
|
+
# Performs an HTTP POST request to the specified path (relative to self.instance_url). The body of the request is taken from _data_.
|
286
|
+
# Query parameters are included from _parameters_. The required +Authorization+ header is automatically included, as are any additional
|
287
|
+
# headers specified in _headers_. Returns the HTTPResult if it is of type HTTPSuccess- raises SalesForceError otherwise.
|
288
|
+
def http_post(path, data=nil, parameters={}, headers={})
|
289
|
+
req = Net::HTTP.new(URI.parse(self.instance_url).host, 443)
|
290
|
+
req.use_ssl = true
|
291
|
+
path_parameters = (parameters || {}).collect { |k, v| "#{URI.escape(k.to_s)}=#{URI.escape(v.to_s)}" }.join('&')
|
292
|
+
encoded_path = [URI.escape(path), path_parameters.empty? ? nil : path_parameters].compact.join('?')
|
293
|
+
log_request(encoded_path, data)
|
294
|
+
result = req.post(encoded_path, data, {"Content-Type" => data ? "application/json" : "text/plain", "Authorization" => "OAuth #{self.oauth_token}"}.merge(headers))
|
295
|
+
log_response(result)
|
296
|
+
raise SalesForceError.new(result) unless result.is_a?(Net::HTTPSuccess)
|
297
|
+
result
|
298
|
+
end
|
299
|
+
|
300
|
+
# Performs an HTTP PATCH request to the specified path (relative to self.instance_url). The body of the request is taken from _data_.
|
301
|
+
# Query parameters are included from _parameters_. The required +Authorization+ header is automatically included, as are any additional
|
302
|
+
# headers specified in _headers_. Returns the HTTPResult if it is of type HTTPSuccess- raises SalesForceError otherwise.
|
303
|
+
def http_patch(path, data=nil, parameters={}, headers={})
|
304
|
+
req = Net::HTTP.new(URI.parse(self.instance_url).host, 443)
|
305
|
+
req.use_ssl = true
|
306
|
+
path_parameters = (parameters || {}).collect { |k, v| "#{URI.escape(k.to_s)}=#{URI.escape(v.to_s)}" }.join('&')
|
307
|
+
encoded_path = [URI.escape(path), path_parameters.empty? ? nil : path_parameters].compact.join('?')
|
308
|
+
log_request(encoded_path, data)
|
309
|
+
result = req.send_request("PATCH", encoded_path, data, {"Content-Type" => data ? "application/json" : "text/plain", "Authorization" => "OAuth #{self.oauth_token}"}.merge(headers))
|
310
|
+
log_response(result)
|
311
|
+
raise SalesForceError.new(result) unless result.is_a?(Net::HTTPSuccess)
|
312
|
+
result
|
313
|
+
end
|
314
|
+
|
315
|
+
# Performs an HTTP POST request to the specified path (relative to self.instance_url), using Content-Type multiplart/form-data.
|
316
|
+
# The parts of the body of the request are taken from parts_. Query parameters are included from _parameters_. The required
|
317
|
+
# +Authorization+ header is automatically included, as are any additional headers specified in _headers_.
|
318
|
+
# Returns the HTTPResult if it is of type HTTPSuccess- raises SalesForceError otherwise.
|
319
|
+
def http_multipart_post(path, parts, parameters={}, headers={})
|
320
|
+
req = Net::HTTP.new(URI.parse(self.instance_url).host, 443)
|
321
|
+
req.use_ssl = true
|
322
|
+
path_parameters = (parameters || {}).collect { |k, v| "#{URI.escape(k.to_s)}=#{URI.escape(v.to_s)}" }.join('&')
|
323
|
+
encoded_path = [URI.escape(path), path_parameters.empty? ? nil : path_parameters].compact.join('?')
|
324
|
+
log_request(encoded_path)
|
325
|
+
result = req.request(Net::HTTP::Post::Multipart.new(encoded_path, parts, {"Authorization" => "OAuth #{self.oauth_token}"}.merge(headers)))
|
326
|
+
log_response(result)
|
327
|
+
raise SalesForceError.new(result) unless result.is_a?(Net::HTTPSuccess)
|
328
|
+
result
|
329
|
+
end
|
330
|
+
|
331
|
+
private
|
332
|
+
|
333
|
+
def log_request(path, data=nil)
|
334
|
+
puts "***** REQUEST: #{path.include?(':') ? path : URI.join(self.instance_url, path)}#{data ? " => #{data}" : ''}" if self.debugging
|
335
|
+
end
|
336
|
+
|
337
|
+
def log_response(result)
|
338
|
+
puts "***** RESPONSE: #{result.class.name} -> #{result.body}" if self.debugging
|
339
|
+
end
|
340
|
+
|
341
|
+
def find_or_materialize(class_or_classname)
|
342
|
+
if class_or_classname.is_a?(Class)
|
343
|
+
clazz = class_or_classname
|
344
|
+
else
|
345
|
+
match = class_or_classname.match(/(?:(.+)::)?(\w+)$/)
|
346
|
+
preceding_namespace = match[1]
|
347
|
+
classname = match[2]
|
348
|
+
raise ArgumentError if preceding_namespace && preceding_namespace != module_namespace.name
|
349
|
+
clazz = module_namespace.const_get(classname.to_sym) rescue nil
|
350
|
+
clazz ||= self.materialize(classname)
|
351
|
+
end
|
352
|
+
clazz
|
353
|
+
end
|
354
|
+
|
355
|
+
def module_namespace
|
356
|
+
self.sobject_module || Object
|
357
|
+
end
|
358
|
+
|
359
|
+
def collection_from(response)
|
360
|
+
response = JSON.parse(response)
|
361
|
+
array_response = response.is_a?(Array)
|
362
|
+
if array_response
|
363
|
+
records = response.collect { |rec| self.find(rec["attributes"]["type"], rec["Id"]) }
|
364
|
+
else
|
365
|
+
records = response["records"].collect do |record|
|
366
|
+
attributes = record.delete('attributes')
|
367
|
+
new_record = find_or_materialize(attributes["type"]).new
|
368
|
+
record.each do |name, value|
|
369
|
+
field = new_record.description['fields'].find do |field|
|
370
|
+
key_from_label(field["label"]) == name || field["name"] == name
|
371
|
+
end
|
372
|
+
set_value(new_record, field["name"], value, field["type"]) if field
|
373
|
+
end
|
374
|
+
new_record
|
375
|
+
end
|
376
|
+
end
|
377
|
+
|
378
|
+
Databasedotcom::Collection.new(self, array_response ? records.length : response["totalSize"], array_response ? nil : response["nextRecordsUrl"]).concat(records)
|
379
|
+
end
|
380
|
+
|
381
|
+
def set_value(record, attr, value, attr_type)
|
382
|
+
value_to_set = value
|
383
|
+
|
384
|
+
case attr_type
|
385
|
+
when "datetime"
|
386
|
+
value_to_set = DateTime.parse(value) rescue nil
|
387
|
+
|
388
|
+
when "date"
|
389
|
+
value_to_set = Date.parse(value) rescue nil
|
390
|
+
|
391
|
+
when "multipicklist"
|
392
|
+
value_to_set = value.split(";") rescue []
|
393
|
+
end
|
394
|
+
|
395
|
+
record.send("#{attr}=", value_to_set)
|
396
|
+
end
|
397
|
+
|
398
|
+
def coerced_json(attrs, clazz)
|
399
|
+
if attrs.is_a?(Hash)
|
400
|
+
coerced_attrs = {}
|
401
|
+
attrs.keys.each do |key|
|
402
|
+
case clazz.field_type(key)
|
403
|
+
when "multipicklist"
|
404
|
+
coerced_attrs[key] = (attrs[key] || []).join(';')
|
405
|
+
when "datetime"
|
406
|
+
coerced_attrs[key] = attrs[key] ? attrs[key].strftime("%Y-%m-%dT%H:%M:%S.%L%z") : nil
|
407
|
+
else
|
408
|
+
coerced_attrs[key] = attrs[key]
|
409
|
+
end
|
410
|
+
end
|
411
|
+
coerced_attrs.to_json
|
412
|
+
else
|
413
|
+
attrs
|
414
|
+
end
|
415
|
+
end
|
416
|
+
|
417
|
+
def key_from_label(label)
|
418
|
+
label.gsub(' ', '_')
|
419
|
+
end
|
420
|
+
end
|
421
|
+
end
|