lib/json_api/controllers/concerns/resource_actions/include_preloading.rb



# frozen_string_literal: true

module JSONAPI
  module ResourceActions
    module IncludePreloading
      extend ActiveSupport::Concern

      private

      def includes_to_hash(paths)
        hash = paths.each_with_object({}) do |path, h|
          path.split(".").reduce(h) { |cur, part| cur[part.to_sym] ||= {} }
        end
        filter_includable(hash, model_class)
      end

      # Preloads associations from the include param so the serializer avoids N+1 queries
      # when walking include paths (association.loaded? is true). Used by both show and index.
      def scope_with_includes(scope)
        includes = parse_include_param
        return scope unless includes.any?

        inc_hash = includes_to_hash(includes)
        hash_contains_polymorphic?(inc_hash, model_class) ? scope.preload(inc_hash) : scope.includes(inc_hash)
      end

      def filter_includable(hash, klass)
        hash.each_with_object({}) do |(key, value), filtered|
          assoc = klass.reflect_on_association(key)
          next unless assoc

          filtered[key] = value.empty? || assoc.polymorphic? ? value : filter_includable(value, assoc.klass)
        end
      end

      def hash_contains_polymorphic?(hash, klass)
        hash.any? { |key, value| polymorphic_in_hash_entry?(key, value, klass) }
      end

      def polymorphic_in_hash_entry?(key, value, klass)
        assoc = klass.reflect_on_association(key)
        return false unless assoc

        assoc.polymorphic? || (value.present? && hash_contains_polymorphic?(value, assoc.klass))
      end

      def preload_included_resource_associations(resources, includes)
        return if includes.empty? || resources.empty?

        includes.each do |include_path|
          association_name = include_path.split(".").first.to_sym
          assoc_reflection = model_class.reflect_on_association(association_name)
          next unless assoc_reflection

          targets = collect_include_targets(resources, association_name)
          next if targets.empty?

          apply_preloads_for_targets(targets, assoc_reflection.polymorphic?)
        end
      end

      def apply_preloads_for_targets(targets, polymorphic)
        if polymorphic
          targets.group_by(&:class).each_value { |recs| apply_resource_preloads(recs) }
        else
          apply_resource_preloads(targets)
        end
      end

      def collect_include_targets(resources, association_name)
        resources.flat_map do |r|
          target = r.association(association_name).target
          target.respond_to?(:to_a) ? target.to_a : Array(target)
        end.compact.uniq
      end

      def apply_resource_preloads(records)
        return if records.empty?

        klass = records.first.class
        resource_scope = ResourceLoader.find_for_model(klass).records
        preload_values = resource_scope.preload_values + resource_scope.includes_values
        return if preload_values.empty?

        ActiveRecord::Associations::Preloader.new(records:, associations: preload_values).call
      end
    end
  end
end