/ecto_searcher

Totally not an attempt to build Ransack-like search

Primary LanguageElixirMIT LicenseMIT

Ecto Searcher

Build Status Coverage Status Hex pm

EctoSearcher is an attempt to bring dynamicly built queries (hello Ransack) to the world of Ecto.

Installation

Add ecto_searcher to your mix.ex deps:

def deps do
  [
    {:ecto_searcher, "~> 0.2.1"}
  ]
end

Notice

This package is build for sql queries and is tested with PostgreSQL. Every other usage may not work.

Usage

Obviously, EctoSearcher works on top of ecto schemas and ecto repos. It consumes ecto query and adds conditions built from input.

Searching with EctoSearcher.Searcher.search/5 and sorting EctoSearcher.Sorter.sort/5 could be used separately or together.

Searching

To search use EctoSearcher.Searcher.search/4 or EctoSearcher.Searcher.search/5:

Basic usage:

defmodule TotallyNotAPhoenixController do
  def not_some_controller_method() do
    base_query = Ecto.Query.from(MyMegaModel)
    search = %{"name_eq" => "Donald Trump", "description_cont" => "My president"}
    query = EctoSearcher.Searcher.search(base_query, MyMegaModel, search)
    MySuperApp.Repo.all(query)
  end
end

Sorting

To sort use EctoSearcher.Sorter.sort/3 or EctoSearcher.Sorter.sort/5:

defmodule TotallyNotAPhoenixController do
  def not_some_controller_method() do
    base_query = Ecto.Query.from(MyMegaModel)
    sort = %{"field" => "name", "order" => "desc"}
    query = EctoSearcher.Sorter.sort(base_query, MyMegaModel, sort)
    MySuperApp.Repo.all(query)
  end
end

Custom fields and matchers

In case you need to implement custom field queries or custom matchers you can implement custom Mapping (using EctoSearcher.Mapping behaviour):

defmodule MySuperApp.CustomMapping do
  use EctoSearcher.Mapping

  def matchers do
    custom_matchers = %{
      "not_eq" => fn field, value -> Query.dynamic([q], ^field != ^value) end
    }

    ## No magic, just plain data manipulation
    Map.merge(
      custom_matchers,
      EctoSearcher.Mapping.Default.matchers()
    )
  end

  def fields do
    %{
      datetime_field_as_date: %{
        query: Query.dynamic([q], fragment("?::date", q.custom_field)),
        type: :date
      }
    }
  end
end

And use it in EctoSearcher.Searcher.search/5 or EctoSearcher.Sorter.sort/5:

defmodule TotallyNotAPhoenixContext do
  import Ecto.Query
  require Ecto.Query

  def not_some_context_method() do
    search = %{
      "name_eq" => "Donald Trump",
      "datetime_as_date_gteq" => "2016-11-08", "datetime_as_date_lteq" => "2018-08-28",
      "description_not_eq" => "Not my president"
    }

    sort = %{
      "field" => "datetime_as_date_gteq",
      "order" => "desc"
    }

    base_query = from(q in MyMegaModel, where: [q.id < 1984])
    query =
      base_query
      |> EctoSearcher.Searcher.search(MyMegaModel, search, MySuperApp.CustomMapping)
      |> EctoSearcher.Searcher.search(base_query, MyMegaModel, sort, MySuperApp.CustomMapping)
      |> MySuperApp.Repo.all()
  end
end

EctoSearcher.Searcher.search/5 and EctoSearcher.Sorter.sort/5 looks up fields in mapping first, then looks up fields in schema.