GeorgeKaraszi/active_record_extended

View on GitHub
lib/active_record_extended/utilities/order_by.rb

Summary

Maintainability
A
1 hr
Test Coverage
# frozen_string_literal: true

require "active_record"

module ActiveRecordExtended
  module Utilities
    module OrderBy
      def inline_order_by(arel_node, ordering_args)
        return arel_node unless scope_preprocess_order_args(ordering_args)

        Arel::Nodes::InfixOperation.new("ORDER BY", arel_node, ordering_args)
      end

      def scope_preprocess_order_args(ordering_args)
        return false if ordering_args.blank? || !@scope.respond_to?(:preprocess_order_args, true)

        # Sanitation check / resolver (ActiveRecord::Relation#preprocess_order_args)
        @scope.send(:preprocess_order_args, ordering_args)
        ordering_args
      end

      # Processes "ORDER BY" expressions for supported aggregate functions
      def order_by_expression(order_by)
        return false unless order_by && order_by.presence.present?

        to_ordered_table_path(order_by)
          .tap { |order_args| process_ordering_arguments!(order_args) }
          .tap { |order_args| scope_preprocess_order_args(order_args) }
      end

      #
      # Turns a hash into a dot notation path.
      #
      # Example:
      # - Using pre-set directions:
      #   [{ products: { position: :asc, id: :desc } }]
      #     #=> [{ "products.position" => :asc, "products.id" => :desc }]
      #
      # - Using fallback directions:
      #   [{products: :position}]
      #     #=> [{"products.position" => :asc}]
      #
      def to_ordered_table_path(args)
        flatten_safely(Array.wrap(args)) do |arg|
          next arg unless arg.is_a?(Hash)

          arg.each_with_object({}) do |(tbl_or_col, obj), new_hash|
            if obj.is_a?(Hash)
              obj.each_pair do |o_key, o_value|
                new_hash["#{tbl_or_col}.#{o_key}"] = o_value
              end
            elsif ActiveRecord::QueryMethods::VALID_DIRECTIONS.include?(obj)
              new_hash[tbl_or_col] = obj
            elsif obj.nil?
              new_hash[tbl_or_col.to_s] = :asc
            else
              new_hash["#{tbl_or_col}.#{obj}"] = :asc
            end
          end
        end
      end

      def process_ordering_arguments!(ordering_args)
        ordering_args.flatten!
        ordering_args.compact!
        ordering_args.map! do |arg|
          next to_arel_sql(arg) unless arg.is_a?(Hash) # ActiveRecord will reflect if an argument is a symbol

          arg.each_with_object({}) do |(field, dir), ordering_obj|
            # ActiveRecord will not reflect if the Hash keys are a `Arel::Nodes::SqlLiteral` klass
            ordering_obj[to_arel_sql(field)] = dir.to_s.downcase
          end
        end
      end
    end
  end
end