View Source AshQueryBuilder

A simple query builder helper for Ash.Query

Installation

If available in Hex, the package can be installed by adding ash_query_builder to your list of dependencies in mix.exs:

def deps do
  [
    {:ash_query_builder, "~> 0.7.0"}
  ]
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:

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:

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:

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)