README.md

# AshQueryBuilder

**A simple query builder helper for Ash.Query**

## Installation

If [available in Hex](https://hex.pm/docs/publish), the package can be installed
by adding `ash_query_builder` to your list of dependencies in `mix.exs`:

```elixir
def deps do
  [
    {:ash_query_builder, "~> 0.6.2"}
  ]
end
```
## Usage

`AshQueryBuilder` is a helper to make it easy to serialize/deserialize URL queries into a structure that can be used to generate a `Ash.Query` with filters and sorting. This is mainly useful when you need to create tables that can be sorted or filtered.

Example:

``` elixir
alias Plug.Conn.Query

# We first create our builder struct
builder = AshQueryBuilder.new()

# Now we can add multiple types of filters to it.
{builder, filter_1} = AshQueryBuilder.add_filter(builder, :updated_at, :<, DateTime.utc_now(), id: "my_custom_id")
{builder, filter_2} = AshQueryBuilder.add_filter(builder, :first_name, "in", ["blibs", "blobs"], [])
{builder, _} = AshQueryBuilder.add_filter(builder, [:organization], :name, :ilike, "MyOrg", enabled?: false)
{builder, _} = AshQueryBuilder.add_filter(builder, :created_by, :is_nil, nil, [])
{builder, _} = AshQueryBuilder.add_filter(builder, :surname, :left_word_similarity, "blobs", [])

# We can also add sorting rules
{builder, sorter_1} = AshQueryBuilder.add_sorter(builder, :updated_at, :desc)
{builder, sorter_2} = AshQueryBuilder.add_sorter(builder, :first_name, :asc)

# This will generate a map that can be stored into a URL query parameters
query_params = AshQueryBuilder.to_params(builder, with_disabled?: true)

# This will generate the URL query parameters, it is similar to just calling ~p"my_url?#{query_params}"
url_query_params = Query.encode(query_params)

# Now we can decode the query back and parse it into a new builder
builder = url_query_params |> Query.decode |> AshQueryBuilder.parse()

# Finally we can use the builder to create the actual Ash.Query
query = Ash.Query.new(Example.MyApi.User)

query = AshQueryBuilder.to_query(builder, query)

# And run the query
Example.MyApi.read!(query)

# We can also remove filters and sorters by id
builder = AshQueryBuilder.remove_filter(builder, filter_1.id)
builder = AshQueryBuilder.remove_sorter(builder, sorter_1.id)

# And replace existing ones
{:error, :not_found} = AshQueryBuilder.replace_filter(builder, filter_1.id, :updated_at, :<, DateTime.utc_now(), [])
{:ok, builder} = AshQueryBuilder.replace_filter(builder, filter_2.id, :first_name, :in, ["blibs", "blubs"], [])

{:error, :not_found} = AshQueryBuilder.replace_sorter(builder, sorter_1.id, :updated_at, :asc, [])
{:ok, builder} = AshQueryBuilder.replace_sorter(builder, filter_2.id, :first_name, :desc, [])
```

## Expanding

`AshQueryBuilder` comes already with a lot of filters commonly used in PostgreSQL (you can find all of them in the `lib/ash_query_builder/filter` directory).

If you need some other specific filter that the library don't support out of the box, you can just easily create it. For example, let's say you are using `postgis` and want to filter by radius, you can create a filter for it like this:

``` elixir
defmodule MyFilter do
  @moduledoc false

  use AshQueryBuilder.Filter, operator: :by_radius

  @impl true
  def new(id, path, field, value, opts) do
    enabled? = Keyword.get(opts, :enabled?, true)

    struct(__MODULE__, id: id, field: field, path: path, value: value, enabled?: enabled?)
  end
end

defimpl AshQueryBuilder.Filter.Protocol, for: MyFilter do
  use AshQueryBuilder.Filter.QueryHelpers

  def to_filter(filter, query) do
    {longitude, latitude, distance_in_meters} = filter.value

    Ash.Query.filter(
      query,
      expr(
        fragment(
          "ST_DWithin(?, ST_POINT(?, ?)::geography, ?)",
          ^make_ref(filter),
          ^longitude,
          ^latitude,
          ^distance_in_meters
        )
      )
    )
  end

  def operator(_), do: MyFilter.operator()
end
```

And then you can use it like this:

``` elixir
builder = AshQueryBuilder.add_filter(builder, :geometry, :by_radius, {-86.79, 36.17, 1000})
```


alias AshQueryBuilder.{Filter, FilterScope, Sorter}

alias Plug.Conn.Query

# We first create our builder struct
builder = AshQueryBuilder.new()

filter_1 = Filter.new(:male_content, :left_word_similarity, "blibs", id: "male_content")
filter_2 = Filter.new(:female_content, :==, "blibs", id: "female_content")

scope_1 = FilterScope.new(:and, id: "content") |> FilterScope.add_filter(filter_1) |> FilterScope.add_filter(filter_2)

filter_3 = Filter.new(:updated_at, :<, DateTime.utc_now(), id: "updated_at")

scope_2 = FilterScope.new(:or, id: "empty")

filter_4 = Filter.new(:inserted_at, :<, DateTime.utc_now(), id: "inserted_at")

scope_3 = FilterScope.new(:or, id: "blibs") |> FilterScope.add_filter(scope_1) |> FilterScope.add_filter(filter_4)

builder = builder |> AshQueryBuilder.add_filter(scope_3) |> AshQueryBuilder.add_filter(filter_3) |> AshQueryBuilder.add_filter(scope_2)

# This will generate a map that can be stored into a URL query parameters
query_params = AshQueryBuilder.to_params(builder, with_disabled?: true)

builder = AshQueryBuilder.parse(query_params)

query = Ash.Query.new(FeedbackCupcake.Feedbacks.Template)

query = AshQueryBuilder.to_query(builder, query)