module ActiveZuora class Relation attr_accessor :selected_field_names, :filters, :order_attribute, :order_direction attr_reader :zobject_class def initialize(zobject_class, selected_field_names=[:id]) @zobject_class, @selected_field_names, @filters = zobject_class, selected_field_names, [] @order_attribute, @order_direction = :created_date, :asc end def dup dup = super dup.selected_field_names = dup.selected_field_names.dup dup.filters = dup.filters.dup dup.unload dup end # # Conditions / Selecting # def select(*field_names) dup.tap { |dup| dup.selected_field_names = field_names.flatten } end def where(conditions) dup.tap { |dup| dup.filters << ['and', conditions] } end alias :and :where def or(conditions) dup.tap { |dup| dup.filters << ['or', conditions] }\ end def order(attribute, direction = :asc) dup.tap do |dup| dup.order_attribute = attribute dup.order_direction = direction end end def scoped # Account.select(:id).where(:status => "Draft") do # Account.all # => select id from Account where status = "Draft" # end previous_scope, zobject_class.current_scope = zobject_class.current_scope, self yield ensure zobject_class.current_scope = previous_scope end def merge(relation) if relation.is_a?(Hash) where(relation) else dup.tap do |dup| dup.filters.concat relation.filters dup.filters.uniq! dup.order_attribute = relation.order_attribute dup.order_direction = relation.order_direction end end end # # Finding / Loading # def to_zql select_statement + " from " + zobject_class.zuora_object_name + " " + where_statement end def find(id) return nil if id.blank? where(:id => id).first end def find_each(&block) # Iterate through each item, but don't save the results in memory. if loaded? # If we're already loaded, iterate through the cached records. to_a.each(&block) else query.each(&block) end end def to_a @records ||= query end alias :all :to_a def loaded? !@records.nil? end def unload @records = nil self end def reload unload.to_a self end def query(&block) # Keep querying until all pages are retrieved. # Throws an exception for an invalid query. response = zobject_class.connection.request(:query){ |soap| soap.body = { :query_string => to_zql } } query_response = response[:query_response] records = objectify_query_results(query_response[:result][:records]) records.each(&:block) if block_given? # If there are more pages of records, keep fetching # them until done. until query_response[:result][:done] query_response = zobject_class.connection.request(:query_more) do |soap| soap.body = { :query_locator => response[:query_response][:result][:query_locator] } end[:query_more_response] more_records = objectify_query_results(query_response[:result][:records]) more_records.each(&:block) if block_given? records.concat more_records end sort_records!(records) rescue Savon::SOAP::Fault => exception # Add the zql to the exception message and re-raise. exception.message << ": #{to_zql}" raise end # # Updating / Deleting # def update_all(attributes={}) # Update using an attribute hash, or you can pass a block # and update the attributes directly on the objects. if block_given? to_a.each { |record| yield record } else to_a.each { |record| record.attributes = attributes } end zobject_class.update(to_a) end def delete_all zobject_class.delete(to_a.map(&:id)) end protected def method_missing(method, *args, &block) # This is how the chaing can happen on class methods or named scopes on the # ZObject class. if Array.method_defined?(method) to_a.send(method, *args, &block) elsif zobject_class.respond_to?(method) scoped { zobject_class.send(method, *args, &block) } else super end end # # Helper methods to build the ZQL. # def select_statement "select " + selected_field_names.map { |field_name| zuora_field_name(field_name) }.join(', ') end def where_statement return '' if @filters.empty? tokens = [] @filters.each do |logical_operator, conditions| if conditions.is_a?(Hash) conditions.each do |field_name, comparisons| zuora_field_name = zuora_field_name(field_name) comparisons = { '=' => comparisons } unless comparisons.is_a?(Hash) comparisons.each do |operator, value| tokens.concat [logical_operator, zuora_field_name, operator, escape_filter_value(value)] end end else tokens.concat [logical_operator, conditions.to_s] end end tokens[0] = "where" tokens.join ' ' end def zuora_field_name(name) zobject_class.get_field!(name).zuora_name end def escape_filter_value(value) if value.nil? "null" elsif value.is_a?(String) "'#{value.gsub("'","\\\\'")}'" elsif value.is_a?(DateTime) || value.is_a?(Time) # If we already have a DateTime or Time, use the zone it already has. escape_filter_value(value.strftime("%FT%T%:z")) # 2007-11-19T08:37:48-06:00 elsif value.is_a?(Date) # Create a DateTime from the date using Zuora's timezone. escape_filter_value(value.to_datetime.change(:offset => "+0800")) else value end end def objectify_query_results(results) return [] if results.blank? # Sometimes Zuora will return only a single record, not in an array. results = [results] unless results.is_a?(Array) results.map do |attributes| # Strip any noisy attributes from the results that have to do with # SOAP namespaces. attributes.delete_if { |key, value| key.to_s.start_with? "@" } # Instantiate the zobject class, but don't track the changes. zobject_class.new(attributes).tap { |record| record.changed_attributes.clear } end end def sort_records!(records) return records unless order_attribute.present? records.sort! do |a, b| if a.nil? -1 elsif b.nil? 1 else a.send(order_attribute) <=> b.send(order_attribute) end end records.reverse! if order_direction == :desc records end end end