ManageIQ/manageiq-api-common

View on GitHub
lib/insights/api/common/paginated_response_v2.rb

Summary

Maintainability
A
35 mins
Test Coverage
module Insights
  module API
    module Common
      class PaginatedResponseV2 < PaginatedResponse
        # GraphQL name regex: /[_A-Za-z][_0-9A-Za-z]*/
        ASSOCIATION_COUNT_ATTR = "__count".freeze

        attr_reader :limit, :offset, :sort_by

        def records
          @records ||= begin
            res = @base_query.order(:id).limit(limit).offset(offset)

            select_for_associations, group_by_associations = sort_by_associations_query_parameters
            res = res.select(*select_for_associations)          if select_for_associations.present?
            res = res.left_outer_joins(*sort_by_associations)   if sort_by_associations.present?
            res = res.group(group_by_associations)              if group_by_associations.present?

            order_options = sort_by_options(res.klass)
            res = res.reorder(order_options) if order_options.present?
            res
          end
        end

        # Condenses parameter values for handling multi-level associations
        # and returns an array of key, value pairs.
        #
        # Examples:
        #
        # Input:  { "association" => { "attribute" => "value" }, "direct_attribute" => "value2" }
        # Output: [["association.attribute", "value"], ["direct_attribute", "value2"]]
        #
        # Input:  { "association" => { "attribute" => "value" }, "association2" => { "attribute2" => "value2" } }
        # Output: [["association.attribute", "value"], ["association2.attribute2", "value2"]]
        #
        # Input:  { "association" => { "attribute1" => "value1", "attribute2" => "value2" } }
        # Output: [["association.attribute1", "value1"], ["association.attribute2", "value2"]]
        #
        def compact_parameter(param)
          result = []
          return result if param.blank?

          param.each do |k, v|
            result += if v.kind_of?(Hash) || v.kind_of?(ActionController::Parameters)
                        Hash(v).map { |ak, av| ["#{k}.#{ak}", av] }
                      else
                        [[k, v]]
                      end
          end
          result
        end

        private

        def sort_by_options(model)
          @sort_by_options ||= begin
            compact_parameter(sort_by).collect do |sort_attr, sort_order|
              sort_order = "asc" if sort_order.blank?
              arel = if sort_attr.include?('.')
                       association, sort_attr = sort_attr.split('.')
                       association_class = association.classify.constantize
                       if sort_attr == ASSOCIATION_COUNT_ATTR
                         Arel.sql("COUNT (#{association_class.table_name}.id)")
                       else
                         association_class.arel_attribute(sort_attr)
                       end
                     else
                       model.arel_attribute(sort_attr)
                     end
              (sort_order == "desc") ? arel.desc : arel.asc
            end
          end
        end

        def sort_by_associations
          @sort_by_associations ||= begin
            compact_parameter(sort_by).collect do |sort_attr, sort_order|
              next unless sort_attr.include?('.')

              sort_attr.split('.').first.to_sym
            end.compact.uniq
          end
        end

        def sort_by_associations_query_parameters
          select_for_associations = []
          group_by_associations   = []
          count_selects           = []

          compact_parameter(sort_by).each do |sort_attr, _sort_order|
            next unless sort_attr.include?('.')

            association, attr = sort_attr.split('.')

            base_id  = "#{@base_query.table_name}.id"
            base_all = "#{@base_query.table_name}.*"
            select_for_associations << base_id << base_all if select_for_associations.empty?
            group_by_associations   << base_id << base_all if group_by_associations.empty?

            if attr == ASSOCIATION_COUNT_ATTR
              count_selects << Arel.sql("COUNT (#{association.classify.constantize.table_name}.id)")
            else
              arel_attr = association.classify.constantize.arel_attribute(attr)
              association_attr = "#{association}_#{attr}"
              select_for_associations << arel_attr.as(association_attr)
              group_by_associations   << association_attr
            end
          end
          select_for_associations.append(*count_selects) unless count_selects.empty?

          [select_for_associations.compact.uniq, group_by_associations.compact.uniq]
        end

        def validate_sort_by
          return unless sort_by.present?
          raise ArgumentError, "Invalid sort_by parameter specified \"#{sort_by}\"" unless sort_by.kind_of?(ActionController::Parameters) || sort_by.kind_of?(Hash)

          compact_parameter(sort_by).each { |sort_attr, sort_order| validate_sort_by_directive(sort_attr, sort_order) }
        end

        def validate_sort_by_directive(sort_attr, sort_order)
          order = sort_order.blank? ? "asc" : sort_order
          raise ArgumentError, "Invalid sort_by directive specified \"#{sort_attr}=#{sort_order}\"" unless sort_attr.match?(/^[a-z\-_\.]+$/) && order.match?(/^(asc|desc)$/)
        end
      end
    end
  end
end