1 module Databasedotcom
2 module Sobject
3 # Parent class of dynamically created sobject types. Interacts with Force.com through a Client object that is passed in during materialization.
4 class Sobject
5 cattr_accessor :client
6 extend ActiveModel::Naming if defined?(ActiveModel::Naming)
7
8 def ==(other)
9 return false unless other.is_a?(self.class)
10 self.Id == other.Id
11 end
12
13 # Returns a new Sobject. The default values for all attributes are set based on its description.
14 def initialize(attrs = {})
15 super()
16 self.class.description["fields"].each do |field|
17 self.send("#{field["name"]}=", field["defaultValueFormula"])
18 end
19 self.attributes=(attrs)
20 end
21
22 # Returns a hash representing the state of this object
23 def attributes
24 self.class.attributes.inject({}) do |hash, attr|
25 hash[attr] = self.send(attr.to_sym)
26 hash
27 end
28 end
29
30 # Set attributes of this object, from a hash, in bulk
31 def attributes=(attrs)
32 attrs.each do |key, value|
33 self.send("#{key}=", value)
34 end
35 end
36
37 # Returns true if the object has been persisted in the Force.com database.
38 def persisted?
39 !self.Id.nil?
40 end
41
42 # Returns true if this record has not been persisted in the Force.com database.
43 def new_record?
44 !self.persisted?
45 end
46
47 # Returns self.
48 def to_model
49 self
50 end
51
52 # Returns a unique object id for self.
53 def to_key
54 [object_id]
55 end
56
57 # Returns the Force.com Id for this instance.
58 def to_param
59 self.Id
60 end
61
62 # Updates the corresponding record on Force.com by setting the attribute +attr_name+ to +attr_value+.
63 #
64 # client.materialize("Car")
65 # c = Car.new
66 # c.update_attribute("Color", "Blue")
67 def update_attribute(attr_name, attr_value)
68 update_attributes(attr_name => attr_value)
69 end
70
71 # Updates the corresponding record on Force.com with the attributes specified by the +new_attrs+ hash.
72 #
73 # client.materialize("Car")
74 # c = Car.new
75 # c.update_attributes {"Color" => "Blue", "Year" => "2012"}
76 def update_attributes(new_attrs)
77 if self.client.update(self.class, self.Id, new_attrs)
78 new_attrs = new_attrs.is_a?(Hash) ? new_attrs : JSON.parse(new_attrs)
79 new_attrs.each do |attr, value|
80 self.send("#{attr}=", value)
81 end
82 end
83 self
84 end
85
86 # Updates the corresponding record on Force.com with the attributes of self.
87 #
88 # client.materialize("Car")
89 # c = Car.find_by_Color("Yellow")
90 # c.Color = "Green"
91 # c.save
92 #
93 # _options_ can contain the following keys:
94 #
95 # exclusions # an array of field names (case sensitive) to exclude from save
96 def save(options={})
97 attr_hash = {}
98 selection_attr = self.Id.nil? ? "createable" : "updateable"
99 self.class.description["fields"].select { |f| f[selection_attr] }.collect { |f| f["name"] }.each { |attr| attr_hash[attr] = self.send(attr) }
100
101 # allow fields to be removed on a case by case basis as some data is not allowed to be saved
102 # (e.g. Name field on Account with record type of Person Account) despite the API listing
103 # some fields as editable
104 if options[:exclusions] and options[:exclusions].respond_to?(:include?) then
105 attr_hash.delete_if { |key, value| options[:exclusions].include?(key.to_s) }
106 end
107
108 if self.Id.nil?
109 self.Id = self.client.create(self.class, attr_hash).Id
110 else
111 self.client.update(self.class, self.Id, attr_hash)
112 end
113 end
114
115 # Deletes the corresponding record from the Force.com database. Returns self.
116 #
117 # client.materialize("Car")
118 # c = Car.find_by_Color("Yellow")
119 # c.delete
120 def delete
121 if self.client.delete(self.class, self.Id)
122 self
123 end
124 end
125
126 # Reloads the record from the Force.com database. Returns self.
127 #
128 # client.materialize("Car")
129 # c = Car.find_by_Color("Yellow")
130 # c.reload
131 def reload
132 self.attributes = self.class.find(self.Id).attributes
133 self
134 end
135
136 # Get a named attribute on this object
137 def [](attr_name)
138 self.send(attr_name) rescue nil
139 end
140
141 # Set a named attribute on this object
142 def []=(attr_name, value)
143 raise ArgumentError.new("No attribute named #{attr_name}") unless self.class.attributes.include?(attr_name)
144 self.send("#{attr_name}=", value)
145 end
146
147 # Returns an Array of attribute names that this Sobject has.
148 #
149 # client.materialize("Car")
150 # Car.attributes #=> ["Id", "Name", "Color", "Year"]
151 def self.attributes
152 self.description["fields"].collect { |f| [f["name"], f["relationshipName"]] }.flatten.compact
153 end
154
155 # Materializes the dynamically created Sobject class by adding all attribute accessors for each field as described in the description of the object on Force.com
156 def self.materialize(sobject_name)
157 self.cattr_accessor :description
158 self.cattr_accessor :type_map
159 self.cattr_accessor :sobject_name
160
161 self.sobject_name = sobject_name
162 self.description = self.client.describe_sobject(self.sobject_name)
163 self.type_map = {}
164
165 self.description["fields"].each do |field|
166
167 # Register normal fields
168 name = field["name"]
169 register_field( field["name"], field )
170
171 # Register relationship fields.
172 if( field["type"] == "reference" and field["relationshipName"] )
173 register_field( field["relationshipName"], field )
174 end
175
176 end
177 end
178
179 # Returns the Force.com type of the attribute +attr_name+. Raises ArgumentError if attribute does not exist.
180 #
181 # client.materialize("Car")
182 # Car.field_type("Color") #=> "string"
183 def self.field_type(attr_name)
184 self.type_map_attr(attr_name, :type)
185 end
186
187 # Returns the label for the attribute +attr_name+. Raises ArgumentError if attribute does not exist.
188 def self.label_for(attr_name)
189 self.type_map_attr(attr_name, :label)
190 end
191
192 # Returns the possible picklist options for the attribute +attr_name+. If +attr_name+ is not of type picklist or multipicklist, [] is returned. Raises ArgumentError if attribute does not exist.
193 def self.picklist_values(attr_name)
194 self.type_map_attr(attr_name, :picklist_values)
195 end
196
197 # Returns true if the attribute +attr_name+ can be updated. Raises ArgumentError if attribute does not exist.
198 def self.updateable?(attr_name)
199 self.type_map_attr(attr_name, :updateable?)
200 end
201
202 # Returns true if the attribute +attr_name+ can be created. Raises ArgumentError if attribute does not exist.
203 def self.createable?(attr_name)
204 self.type_map_attr(attr_name, :createable?)
205 end
206
207 # Delegates to Client.find with arguments +record_id+ and self
208 #
209 # client.materialize("Car")
210 # Car.find("rid") #=> #<Car @Id="rid", ...>
211 def self.find(record_id)
212 self.client.find(self, record_id)
213 end
214
215 # Returns all records of type self as instances.
216 #
217 # client.materialize("Car")
218 # Car.all #=> [#<Car @Id="1", ...>, #<Car @Id="2", ...>, #<Car @Id="3", ...>, ...]
219 def self.all
220 self.client.query("SELECT #{self.field_list} FROM #{self.sobject_name}")
221 end
222
223 # Returns a collection of instances of self that match the conditional +where_expr+, which is the WHERE part of a SOQL query.
224 #
225 # client.materialize("Car")
226 # Car.query("Color = 'Blue'") #=> [#<Car @Id="1", @Color="Blue", ...>, #<Car @Id="5", @Color="Blue", ...>, ...]
227 def self.query(where_expr)
228 self.client.query("SELECT #{self.field_list} FROM #{self.sobject_name} WHERE #{where_expr}")
229 end
230
231 # Delegates to Client.search
232 def self.search(sosl_expr)
233 self.client.search(sosl_expr)
234 end
235
236 # Find the first record. If the +where_expr+ argument is present, it must be the WHERE part of a SOQL query
237 def self.first(where_expr=nil)
238 where = where_expr ? "WHERE #{where_expr} " : ""
239 self.client.query("SELECT #{self.field_list} FROM #{self.sobject_name} #{where}ORDER BY Id ASC LIMIT 1").first
240 end
241
242 # Find the last record. If the +where_expr+ argument is present, it must be the WHERE part of a SOQL query
243 def self.last(where_expr=nil)
244 where = where_expr ? "WHERE #{where_expr} " : ""
245 self.client.query("SELECT #{self.field_list} FROM #{self.sobject_name} #{where}ORDER BY Id DESC LIMIT 1").first
246 end
247
248 #Delegates to Client.upsert with arguments self, +field+, +values+, and +attrs+
249 def self.upsert(field, value, attrs)
250 self.client.upsert(self.sobject_name, field, value, attrs)
251 end
252
253 # Delegates to Client.delete with arguments +record_id+ and self
254 def self.delete(record_id)
255 self.client.delete(self.sobject_name, record_id)
256 end
257
258 # Get the total number of records
259 def self.count
260 self.client.query("SELECT COUNT() FROM #{self.sobject_name}").total_size
261 end
262
263 # Sobject objects support dynamic finders similar to ActiveRecord.
264 #
265 # client.materialize("Car")
266 # Car.find_by_Color("Blue")
267 # Car.find_all_by_Year("2011")
268 # Car.find_by_Color_and_Year("Blue", "2011")
269 # Car.find_or_create_by_Year("2011")
270 # Car.find_or_initialize_by_Name("Foo")
271 def self.method_missing(method_name, *args, &block)
272 if method_name.to_s =~ /^find_(or_create_|or_initialize_)?by_(.+)$/ || method_name.to_s =~ /^find_(all_)by_(.+)$/
273 named_attrs = $2.split('_and_')
274 attrs_and_values_for_find = {}
275 hash_args = args.length == 1 && args[0].is_a?(Hash)
276 attrs_and_values_for_write = hash_args ? args[0] : {}
277
278 named_attrs.each_with_index do |attr, index|
279 value = hash_args ? args[0][attr] : args[index]
280 attrs_and_values_for_find[attr] = value
281 attrs_and_values_for_write[attr] = value unless hash_args
282 end
283
284 limit_clause = method_name.to_s.include?('_all_by_') ? "" : " LIMIT 1"
285
286 results = self.client.query("SELECT #{self.field_list} FROM #{self.sobject_name} WHERE #{soql_conditions_for(attrs_and_values_for_find)}#{limit_clause}")
287 results = limit_clause == "" ? results : results.first rescue nil
288
289 if results.nil?
290 if method_name.to_s =~ /^find_or_create_by_(.+)$/
291 results = self.client.create(self, attrs_and_values_for_write)
292 elsif method_name.to_s =~ /^find_or_initialize_by_(.+)$/
293 results = self.new
294 attrs_and_values_for_write.each { |attr, val| results.send("#{attr}=", val) }
295 end
296 end
297
298 results
299 else
300 super
301 end
302 end
303
304 # Delegates to Client.create with arguments +object_attributes+ and self
305 def self.create(object_attributes)
306 self.client.create(self, object_attributes)
307 end
308
309 # Coerce values submitted from a Rails form to the values expected by the database
310 # returns a new hash with updated values
311 def self.coerce_params(params)
312 params.each do |attr, value|
313 case self.field_type(attr)
314 when "boolean"
315 params[attr] = value.is_a?(String) ? value.to_i != 0 : value
316 when "currency", "percent", "double"
317 params[attr] = value.to_f
318 when "date"
319 params[attr] = Date.parse(value) rescue Date.today
320 when "datetime"
321 params[attr] = DateTime.parse(value) rescue DateTime.now
322 end
323 end
324 end
325
326 private
327
328 def self.register_field( name, field )
329 public
330 attr_accessor name.to_sym
331 private
332 self.type_map[name] = {
333 :type => field["type"],
334 :label => field["label"],
335 :picklist_values => field["picklistValues"],
336 :updateable? => field["updateable"],
337 :createable? => field["createable"]
338 }
339 end
340
341 def self.field_list
342 self.description['fields'].collect { |f| f['name'] }.join(',')
343 end
344
345 def self.type_map_attr(attr_name, key)
346 raise ArgumentError.new("No attribute named #{attr_name}") unless self.type_map.has_key?(attr_name)
347 self.type_map[attr_name][key]
348 end
349
350 def self.soql_conditions_for(params)
351 params.inject([]) do |arr, av|
352 case av[1]
353 when String
354 value_str = "'#{av[1].gsub("'", "\\\\'")}'"
355 when DateTime, Time
356 value_str = av[1].strftime(RUBY_VERSION.match(/^1.8/) ? "%Y-%m-%dT%H:%M:%S.000%z" : "%Y-%m-%dT%H:%M:%S.%L%z").insert(-3, ":")
357 when Date
358 value_str = av[1].strftime("%Y-%m-%d")
359 else
360 value_str = av[1].to_s
361 end
362
363 arr << "#{av[0]} = #{value_str}"
364 arr
365 end.join(" AND ")
366 end
367 end
368 end
369 end