module JSONAPI::Support::NestedFilters
def apply_column_operator_filter(scope, target_model, filter_name, filter_value)
def apply_column_operator_filter(scope, target_model, filter_name, filter_value) column_filter = parse_column_filter(filter_name) return nil unless column_filter column = target_model.column_for_attribute(column_filter[:column]) return nil unless column value = normalize_filter_value_for_model(target_model, column, filter_value) return nil unless value condition = build_condition(target_model, column, value, column_filter[:operator]) condition ? apply_condition(scope, condition) : nil end
def apply_direct_column_filter(scope, target_model, filter_name, filter_value)
def apply_direct_column_filter(scope, target_model, filter_name, filter_value) return nil unless target_model.column_names.include?(filter_name) scope.where(target_model.table_name => { filter_name => filter_value }) end
def apply_filter_for_path(scope, filter_name, filter_value)
def apply_filter_for_path(scope, filter_name, filter_value) parts = filter_name.split(".") return scope if parts.length < 2 relationship_chain = parts[0..-2] leaf_filter = parts.last result = traverse_relationship_chain(scope, relationship_chain, leaf_filter, filter_value) return result[:scope] if result[:early_return] apply_joined_filter(scope, result, relationship_chain:, leaf_filter:, filter_value:) end
def apply_filter_on_model(scope, target_model, target_resource, filter_name, filter_value)
def apply_filter_on_model(scope, target_model, target_resource, filter_name, filter_value) return scope if empty_filter_value?(filter_value) apply_column_operator_filter(scope, target_model, filter_name, filter_value) || apply_direct_column_filter(scope, target_model, filter_name, filter_value) || apply_scope_method_filter(scope, target_model, target_resource, filter_name, filter_value) || scope end
def apply_joined_filter(scope, result, relationship_chain:, leaf_filter:, filter_value:)
def apply_joined_filter(scope, result, relationship_chain:, leaf_filter:, filter_value:) join_hash = build_join_hash_for_chain(relationship_chain) scope = scope.joins(join_hash) if join_hash.present? apply_filter_on_model(scope, result[:model], result[:definition], leaf_filter, filter_value) end
def apply_nested_relationship_filters(scope)
def apply_nested_relationship_filters(scope) return scope if filter_params.empty? nested_filters = filter_params.select { |k, _v| k.to_s.include?(".") } return scope if nested_filters.empty? nested_filters.reduce(scope) do |current_scope, (filter_name, filter_value)| apply_filter_for_path(current_scope, filter_name.to_s, filter_value) end end
def apply_scope_method_filter(scope, target_model, target_resource, filter_name, filter_value)
def apply_scope_method_filter(scope, target_model, target_resource, filter_name, filter_value) if target_model.respond_to?(filter_name.to_sym) return try_scope_method(scope, target_model, filter_name, filter_value,) end return nil unless target_resource return nil unless target_resource.permitted_filters.map(&:to_s).include?(filter_name) return nil unless target_model.respond_to?(filter_name.to_sym) try_scope_method(scope, target_model, filter_name, filter_value) end
def build_join_hash_for_chain(chain)
def build_join_hash_for_chain(chain) return nil if chain.empty? chain.reverse.reduce(nil) do |acc, name| if acc.nil? name.to_sym else { name.to_sym => acc } end end end
def process_chain_step(scope, current_model, relationship_name, leaf_filter, filter_value)
def process_chain_step(scope, current_model, relationship_name, leaf_filter, filter_value) association = current_model.reflect_on_association(relationship_name.to_sym) return { scope:, early_return: true } unless association if association.polymorphic? attributes = { leaf_filter => filter_value } return { scope: apply_polymorphic_nested_filters(scope, association, relationship_name, attributes), early_return: true, } end next_definition = JSONAPI::Resource.resource_for_model(association.klass) return { scope:, early_return: true } unless next_definition { model: association.klass, definition: next_definition, early_return: false } end
def traverse_relationship_chain(scope, relationship_chain, leaf_filter, filter_value)
def traverse_relationship_chain(scope, relationship_chain, leaf_filter, filter_value) current_model = model_class current_definition = definition relationship_chain.each do |relationship_name| result = process_chain_step(scope, current_model, relationship_name, leaf_filter, filter_value) return result if result[:early_return] current_model = result[:model] current_definition = result[:definition] end { model: current_model, definition: current_definition, early_return: false } end
def try_scope_method(scope, target_model, filter_name, filter_value)
def try_scope_method(scope, target_model, filter_name, filter_value) scope.merge(target_model.public_send(filter_name.to_sym, filter_value)) rescue ArgumentError, NoMethodError nil end