app/controllers/avo/search_controller.rb
require_dependency "avo/application_controller"
module Avo
class SearchController < ApplicationController
include Rails.application.routes.url_helpers
include ActionView::Helpers::TextHelper
before_action :set_resource_name, only: :show
before_action :set_resource, only: :show
def show
render json: search_resources([resource])
rescue => error
render_search_error(error)
end
private
def search_resources(resources)
resources
.map do |resource|
# Apply authorization
next unless @authorization.set_record(resource.model_class).authorize_action(:search, raise_exception: false)
# Filter out the models without a search_query
next if resource.search_query.nil?
search_resource resource
end
.select do |payload|
payload.present?
end
.sort_by do |payload|
payload.last[:count]
end
.reverse
.to_h
end
def search_resource(resource)
query = Avo::ExecutionContext.new(
target: resource.search_query,
params: params,
query: resource.query_scope
).handle
query = apply_scope(query) if should_apply_any_scope?
# Get the count
results_count = query.reselect(resource.model_class.primary_key).count
# Get the results
query = query.limit(8)
results = apply_search_metadata(query, resource)
header = resource.plural_name
if results_count > 0
header = "#{header} (#{results_count})"
end
result_object = {
header: header,
help: resource.fetch_search(:help) || "",
results: results,
count: results.count
}
[resource.name.pluralize.downcase, result_object]
end
# When searching in a `has_many` association and will scope out the records against the parent record.
# This is also used when looking for `belongs_to` associations, and this method applies the parents `attach_scope` if present.
def apply_scope(query)
if should_apply_has_many_scope?
apply_has_many_scope
elsif should_apply_attach_scope?
apply_attach_scope(query, parent)
end
end
# Parent passed as argument to be used as a variable instead of the method "def parent"
# Otherwise parent = params...safe_constantize... will try to call method "def parent="
def apply_attach_scope(query, parent)
# If the parent is nil it probably means that someone's creating the record so it's not attached yet.
# In these scenarios, try to find the grandparent for the new views where the parent is nil
# and initialize the parent record with the grandparent attached so the user has the required information
# to scope the query.
# Example usage: Got to a project, create a new review, and search for a user.
if parent.blank? && params[:via_parent_resource_id].present? && params[:via_parent_resource_class].present? && params[:via_relation].present?
parent_resource_class = BaseResource.get_model_by_name params[:via_parent_resource_class]
reflection_class = BaseResource.get_model_by_name params[:via_reflection_class]
grandparent = parent_resource_class.find params[:via_parent_resource_id]
parent = reflection_class.new(
params[:via_relation] => grandparent
)
end
Avo::ExecutionContext.new(target: attach_scope, query: query, parent: parent).handle
end
# This scope is applied if the search is being performed on a has_many association
def apply_has_many_scope
association_name = BaseResource.valid_association_name(parent, params[:via_association_id])
# Get association records
query = parent.send(association_name)
# Apply policy scope if authorization is present
query = resource.authorization&.apply_policy query
Avo::ExecutionContext.new(target: @resource.class.search_query, params: params, query: query).handle
end
def apply_search_metadata(records, avo_resource)
records.map do |record|
resource = avo_resource.new(record: record)
fetch_result_information record, resource, resource.class.fetch_search(:item, record: record)
end
end
def fetch_result_information(record, resource, item)
title = item&.dig(:title) || resource.record_title
highlighted_title = highlight(title&.to_s, params[:q])
record_path = if resource.link_to_child_resource
Avo.resource_manager.get_resource_by_model_class(record.class).new(record: record).record_path
else
resource.record_path
end
{
_id: record.id,
_label: highlighted_title,
_url: resource.class.fetch_search(:result_path, record: resource.record) || record_path
}
end
def should_apply_has_many_scope?
params[:via_association] == "has_many" && @resource.class.search_query.present?
end
def should_apply_attach_scope?
params[:via_association] == "belongs_to" && attach_scope.present?
end
def should_apply_any_scope?
should_apply_has_many_scope? || should_apply_attach_scope?
end
def attach_scope
@attach_scope ||= field&.attach_scope
end
def field
@field ||= fetch_field
end
def parent
@parent ||= fetch_parent
end
def fetch_field
return if params[:via_association_id].nil?
reflection_resource = Avo.resource_manager.get_resource_by_model_class(params[:via_reflection_class]).new(
view: Avo::ViewInquirer.new(params[:via_reflection_view]),
record: parent,
params: params,
user: _current_user
)
reflection_resource.detect_fields.get_field(params[:via_association_id])
end
def fetch_parent
return unless params[:via_reflection_id].present?
parent_resource = Avo.resource_manager.get_resource_by_model_class params[:via_reflection_class]
parent_resource.find_record params[:via_reflection_id], params: params
end
def render_search_error(error)
raise error unless Rails.env.development?
render json: {
error: {
header: "🚨 An error occurred while searching. 🚨",
help: "Please see the error and fix it before deploying.",
results: {
_label: error.message
},
count: 1
}
}, status: 500
end
end
end