module Databasedotcom module Sobject # Parent class of dynamically created sobject types. Interacts with Force.com through a Client object that is passed in during materialization. class Sobject cattr_accessor :client extend ActiveModel::Naming if defined?(ActiveModel::Naming) # Returns a new Sobject. The default values for all attributes are set based on its description. def initialize(attrs = {}) super() self.class.description["fields"].each do |field| self.send("#{field["name"]}=", field["defaultValueFormula"]) end self.attributes=(attrs) end def attributes=(attrs) attrs.each do |key, value| self.send("#{key}=", value) end end # Returns true if the object has been persisted in the Force.com database. def persisted? !self.Id.nil? end # Returns true if this record has not been persisted in the Force.com database. def new_record? !self.persisted? end # Returns self. def to_model self end # Returns a unique object id for self. def to_key [object_id] end # Returns the Force.com Id for this instance. def to_param self.Id end # Updates the corresponding record on Force.com by setting the attribute +attr_name+ to +attr_value+. # # client.materialize("Car") # c = Car.new # c.update_attribute("Color", "Blue") def update_attribute(attr_name, attr_value) update_attributes(attr_name => attr_value) end # Updates the corresponding record on Force.com with the attributes specified by the +new_attrs+ hash. # # client.materialize("Car") # c = Car.new # c.update_attributes {"Color" => "Blue", "Year" => "2012"} def update_attributes(new_attrs) if self.client.update(self.class, self.Id, new_attrs) new_attrs = new_attrs.is_a?(Hash) ? new_attrs : JSON.parse(new_attrs) new_attrs.each do |attr, value| self.send("#{attr}=", value) end end self end # Updates the corresponding record on Force.com with the attributes of self. # # client.materialize("Car") # c = Car.find_by_Color("Yellow") # c.Color = "Green" # c.save def save attr_hash = {} self.class.description["fields"].select { |f| f["updateable"] }.collect { |f| f["name"] }.each { |attr| attr_hash[attr] = self.send(attr) } if self.Id.nil? self.client.create(self.class, attr_hash) else self.client.update(self.class, self.Id, attr_hash) end end # Deletes the corresponding record from the Force.com database. Returns self. # # client.materialize("Car") # c = Car.find_by_Color("Yellow") # c.delete def delete if self.client.delete(self.class, self.Id) self end end # Reloads the record from the Force.com database. Returns the reloaded record. # # client.materialize("Car") # c = Car.find_by_Color("Yellow") # c.reload def reload self.class.find(self.Id) end def [](attr_name) self.send(attr_name) rescue nil end def []=(attr_name, value) raise ArgumentError.new("No attribute named #{attr_name}") unless self.class.attributes.include?(attr_name) self.send("#{attr_name}=", value) end # Returns an Array of attribute names that this Sobject has. # # client.materialize("Car") # Car.attributes #=> ["Id", "Name", "Color", "Year"] def self.attributes self.description["fields"].collect { |f| f["name"] } end # 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 def self.materialize(sobject_name) self.cattr_accessor :description self.cattr_accessor :type_map self.cattr_accessor :sobject_name self.sobject_name = sobject_name self.description = self.client.describe_sobject(self.sobject_name) self.type_map = {} self.description["fields"].each do |field| name = field["name"] attr_accessor name.to_sym self.type_map[name] = {:type => field["type"], :label => field["label"], :picklist_values => field["picklistValues"], :updateable? => field["updateable"]} end end # Returns the Force.com type of the attribute +attr_name+. Raises ArgumentError if attribute does not exist. # # client.materialize("Car") # Car.field_type("Color") #=> "string" def self.field_type(attr_name) self.type_map_attr(attr_name, :type) end # Returns the label for the attribute +attr_name+. Raises ArgumentError if attribute does not exist. def self.label_for(attr_name) self.type_map_attr(attr_name, :label) end # 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. def self.picklist_values(attr_name) self.type_map_attr(attr_name, :picklist_values) end # Returns true if the attribute +attr_name+ can be updated. Raises ArgumentError if attribute does not exist. def self.updateable?(attr_name) self.type_map_attr(attr_name, :updateable?) end # Delegates to Client.find with arguments +record_id+ and self # # client.materialize("Car") # Car.find("rid") #=> # def self.find(record_id) self.client.find(self, record_id) end # Returns all records of type self as instances. # # client.materialize("Car") # Car.all #=> [#, #, #, ...] def self.all self.client.query("SELECT #{self.field_list} FROM #{self.sobject_name}") end # Returns a collection of instances of self that match the conditional +where_expr+, which is the WHERE part of a SOQL query. # # client.materialize("Car") # Car.query("Color = 'Blue'") #=> [#, #, ...] def self.query(where_expr) self.client.query("SELECT #{self.field_list} FROM #{self.sobject_name} WHERE #{where_expr}") end # Delegates to Client.search def self.search(sosl_expr) self.client.search(sosl_expr) end # Find the first record. If the +where_expr+ argument is present, it must be the WHERE part of a SOQL query def self.first(where_expr=nil) where = where_expr ? "WHERE #{where_expr} " : "" self.client.query("SELECT #{self.field_list} FROM #{self.sobject_name} #{where}ORDER BY Id ASC LIMIT 1").first end # Find the last record. If the +where_expr+ argument is present, it must be the WHERE part of a SOQL query def self.last(where_expr=nil) where = where_expr ? "WHERE #{where_expr} " : "" self.client.query("SELECT #{self.field_list} FROM #{self.sobject_name} #{where}ORDER BY Id DESC LIMIT 1").first end #Delegates to Client.upsert with arguments self, +field+, +values+, and +attrs+ def self.upsert(field, value, attrs) self.client.upsert(self.sobject_name, field, value, attrs) end # Delegates to Client.delete with arguments +record_id+ and self def self.delete(record_id) self.client.delete(self.sobject_name, record_id) end # Sobject objects support dynamic finders similar to ActiveRecord. # # client.materialize("Car") # Car.find_by_Color("Blue") # Car.find_all_by_Year("2011") # Car.find_by_Color_and_Year("Blue", "2011") # Car.find_or_create_by_Year("2011") # Car.find_or_initialize_by_Name("Foo") def self.method_missing(method_name, *args, &block) if method_name.to_s =~ /^find_(or_create_|or_initialize_)?by_(.+)$/ || method_name.to_s =~ /^find_(all_)by_(.+)$/ named_attrs = $2.split('_and_') attrs_and_values_for_find = [] hash_args = args.length == 1 && args[0].is_a?(Hash) attrs_and_values_for_write = hash_args ? args[0] : {} named_attrs.each_with_index do |attr, index| value = hash_args ? args[0][attr] : args[index] attrs_and_values_for_find << "#{attr} = '#{value}'" attrs_and_values_for_write[attr] = value unless hash_args end limit_clause = method_name.to_s.include?('_all_by_') ? "" : " LIMIT 1" results = self.client.query("SELECT #{self.field_list} FROM #{self.sobject_name} WHERE #{attrs_and_values_for_find.join(' AND ')}#{limit_clause}") results = limit_clause == "" ? results : results.first rescue nil if results.nil? if method_name.to_s =~ /^find_or_create_by_(.+)$/ results = self.client.create(self, attrs_and_values_for_write) elsif method_name.to_s =~ /^find_or_initialize_by_(.+)$/ results = self.new attrs_and_values_for_write.each { |attr, val| results.send("#{attr}=", val) } end end results else super end end # Delegates to Client.create with arguments +object_attributes+ and self def self.create(object_attributes) self.client.create(self, object_attributes) end # Coerce values submitted from a Rails form to the values expected by the database # returns a new hash with updated values def self.coerce_params(params) params.each do |attr, value| case self.field_type(attr) when "boolean" params[attr] = value.is_a?(String) ? value.to_i != 0 : value when "currency", "percent", "double" params[attr] = value.to_f when "date" params[attr] = Date.parse(value) rescue Date.today when "datetime" params[attr] = DateTime.parse(value) rescue DateTime.now end end end private def self.field_list self.description['fields'].collect { |f| f['name'] }.join(',') end def self.type_map_attr(attr_name, key) raise ArgumentError.new("No attribute named #{attr_name}") unless self.type_map.has_key?(attr_name) self.type_map[attr_name][key] end end end end