databasedotcom_cloudfuji 1.3.1
Sign up to get free protection for your applications and to get access to all the features.
- data/MIT-LICENSE +20 -0
- data/README.rdoc +166 -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 +552 -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 +16 -0
- data/lib/databasedotcom/sales_force_error.rb +26 -0
- data/lib/databasedotcom/sobject.rb +2 -0
- data/lib/databasedotcom/sobject/sobject.rb +376 -0
- data/lib/databasedotcom/version.rb +3 -0
- data/lib/databasedotcom_cloudfuji.rb +1 -0
- metadata +138 -0
@@ -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,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,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
|