Skip to content

Commit

Permalink
refactor query builder api *breaking change* (#60)
Browse files Browse the repository at this point in the history
* refactor query builder api

* add @moduledoc since: 2.5.0
  • Loading branch information
cylkdev authored Oct 16, 2024
1 parent b73678b commit 9653ea7
Show file tree
Hide file tree
Showing 9 changed files with 325 additions and 166 deletions.
30 changes: 21 additions & 9 deletions lib/ecto_shorts/common_filters.ex
Original file line number Diff line number Diff line change
Expand Up @@ -56,18 +56,26 @@ defmodule EctoShorts.CommonFilters do
QueryBuilder
}

@type source :: binary()
@type params :: map() | keyword()
@type adapter :: module()
@type filter_key :: atom()
@type filter_value :: any()
@type source :: binary()
@type query :: Ecto.Query.t()
@type queryable :: Ecto.Queryable.t()
@type source_queryable :: {source(), queryable()}
@type filter_key :: atom()
@type filter_value :: any()

@common_filters QueryBuilder.Common.filters()

@behaviour EctoShorts.QueryBuilder

@doc """
Converts filter params into a query
Converts filter params into a query.
### Examples
iex> EctoShorts.CommonFilters.convert_params_to_filter(EctoShorts.Support.Schemas.Comment, %{id: 1})
#Ecto.Query<from c0 in EctoShorts.Support.Schemas.Comment, where: c0.id == ^1>
"""
@spec convert_params_to_filter(
query :: query() | queryable() | source_queryable(),
Expand Down Expand Up @@ -97,25 +105,29 @@ defmodule EctoShorts.CommonFilters do
create_schema_filter(query, filter_key, filter_value)
end

@impl true
@doc """
Implementation for `c:EctoShorts.QueryBuilder.create_schema_filter/2`.
Implementation for `c:EctoShorts.QueryBuilder.create_schema_filter/3`.
### Examples
iex> EctoShorts.CommonFilters.create_schema_filter(EctoShorts.Support.Schemas.Post, :first, 1_000)
#Ecto.Query<from p0 in EctoShorts.Support.Schemas.Post, limit: ^1000>
iex> EctoShorts.CommonFilters.create_schema_filter(EctoShorts.Support.Schemas.Post, :comments, %{id: 1})
#Ecto.Query<from p0 in EctoShorts.Support.Schemas.Post, join: c1 in assoc(p0, :comments), as: :ecto_shorts_comments, where: c1.id == ^1>
"""
@spec create_schema_filter(
query :: query(),
filter_key :: filter_key(),
filter_value :: filter_value()
) :: query()
def create_schema_filter(query, filter, value) when filter in @common_filters do
QueryBuilder.create_schema_filter(QueryBuilder.Common, {filter, value}, query)
def create_schema_filter(query, filter_key, filter_value) when filter_key in @common_filters do
QueryBuilder.create_schema_filter(QueryBuilder.Common, query, filter_key, filter_value)
end

def create_schema_filter(query, filter, value) do
QueryBuilder.create_schema_filter(QueryBuilder.Schema, {filter, value}, query)
def create_schema_filter(query, filter_key, filter_value) do
QueryBuilder.create_schema_filter(QueryBuilder.Schema, query, filter_key, filter_value)
end

defp ensure_last_is_final_filter(params) do
Expand Down
67 changes: 53 additions & 14 deletions lib/ecto_shorts/query_builder.ex
Original file line number Diff line number Diff line change
@@ -1,20 +1,59 @@
defmodule EctoShorts.QueryBuilder do
@moduledoc "Behaviour for query building from filter tuples"
@moduledoc """
Specifies the query builder API required from adapters.
"""
@moduledoc since: "2.5.0"

@type filter_tuple :: {filter_type :: atom, value :: any}
@type accumulator_query :: Ecto.Query.t
@type adapter :: module()
@type filter_key :: atom()
@type filter_value :: any()
@type query :: Ecto.Query.t()
@type queryable :: Ecto.Queryable.t()

@doc "Adds to accumulator query with filter_type and value"
@callback create_schema_filter(filter_tuple, accumulator_query) :: Ecto.Query.t
@doc """
Adds an expression to they query given a filter key and value.
@spec create_schema_filter(module, filter_tuple, accumulator_query) :: Ecto.Query.t
def create_schema_filter(builder, filter_tuple, query) do
builder.create_schema_filter(filter_tuple, query)
end
The `Ecto.Query` returned should should be same as if it was
written using the `Ecto.Query` dsl.
For example the following function call
```elixir
iex> Ecto.Query.from(c in EctoShorts.Support.Schemas.Comment, where: c.id == 1)
#Ecto.Query<from c0 in EctoShorts.Support.Schemas.Comment, where: c0.id == 1>
```
is equivalent to
```elixir
iex> EctoShorts.QueryBuilder.create_schema_filter(
...> EctoShorts.QueryBuilder.Schema,
...> EctoShorts.Support.Schemas.Comment,
...> :id,
...> 1
...> )
#Ecto.Query<from c0 in EctoShorts.Support.Schemas.Comment, where: c0.id == ^1>
```
"""
@callback create_schema_filter(query(), filter_key(), filter_value()) :: query()

@spec query_schema(Ecto.Queryable.t) :: Ecto.Queryable.t()
@doc "Pulls the schema from a query"
def query_schema(%{from: %{source: {_, schema}}}), do: query_schema(schema)
def query_schema(%{from: %{query: %{from: {_, schema}}}}), do: schema
def query_schema(query), do: query
@doc """
Invokes the callback function `c:EctoShorts.QueryBuilder.create_schema_filter/3`.
Returns an `Ecto.Query`.
### Examples
iex> EctoShorts.QueryBuilder.create_schema_filter(
...> EctoShorts.QueryBuilder.Common,
...> EctoShorts.Support.Schemas.Comment,
...> :first,
...> 1_000
...> )
#Ecto.Query<from c0 in EctoShorts.Support.Schemas.Comment, limit: ^1000>
"""
@spec create_schema_filter(adapter(), query(), filter_key(), filter_value()) :: query()
def create_schema_filter(adapter, query, filter_key, filter_value) do
adapter.create_schema_filter(query, filter_key, filter_value)
end
end
104 changes: 68 additions & 36 deletions lib/ecto_shorts/query_builder/common.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,24 @@ defmodule EctoShorts.QueryBuilder.Common do
This module contains query building parts for common things such
as preload, start/end date and others
"""

@moduledoc since: "2.5.0"
import Logger, only: [debug: 1]
import Ecto.Query, only: [
offset: 2, preload: 2, where: 3, limit: 2,
exclude: 2, from: 2, subquery: 1, order_by: 2
]

alias EctoShorts.QueryBuilder
alias EctoShorts.{
QueryBuilder,
QueryHelpers
}

alias Ecto.Query
require Ecto.Query

@type filter_key :: atom()
@type filter_value :: any()
@type filters :: list(atom())
@type source :: binary()
@type query :: Ecto.Query.t()
@type queryable :: Ecto.Queryable.t()
@type source_queryable :: {source(), queryable()}

@behaviour QueryBuilder

Expand All @@ -29,51 +39,73 @@ defmodule EctoShorts.QueryBuilder.Common do
:order_by
]

@spec filters :: list(atom)
@doc """
Returns the list of supported filters.
### Examples
iex> EctoShorts.QueryBuilder.Common.filters()
[
:preload,
:start_date,
:end_date,
:before,
:after,
:ids,
:first,
:last,
:limit,
:offset,
:search,
:order_by
]
"""
@spec filters :: filters()
def filters, do: @filters

@impl QueryBuilder
def create_schema_filter({:preload, val}, query), do: preload(query, ^val)
@impl true
@doc """
Implementation for `c:EctoShorts.QueryBuilder.create_schema_filter/3`.
### Examples
iex> EctoShorts.QueryBuilder.Common.create_schema_filter(EctoShorts.Support.Schemas.Post, :ids, [1])
"""
@spec create_schema_filter(
query :: query(),
filter_key :: filter_key(),
filter_value :: filter_value()
) :: query()
def create_schema_filter(query, :preload, val), do: Query.preload(query, ^val)

@impl QueryBuilder
def create_schema_filter({:start_date, val}, query), do: where(query, [m], m.inserted_at >= ^(val))
def create_schema_filter(query, :start_date, val), do: Query.where(query, [m], m.inserted_at >= ^(val))

@impl QueryBuilder
def create_schema_filter({:end_date, val}, query), do: where(query, [m], m.inserted_at <= ^val)
def create_schema_filter(query, :end_date, val), do: Query.where(query, [m], m.inserted_at <= ^val)

@impl QueryBuilder
def create_schema_filter({:before, id}, query), do: where(query, [m], m.id < ^id)
def create_schema_filter(query, :before, id), do: Query.where(query, [m], m.id < ^id)

@impl QueryBuilder
def create_schema_filter({:after, id}, query), do: where(query, [m], m.id > ^id)
def create_schema_filter(query, :after, id), do: Query.where(query, [m], m.id > ^id)

@impl QueryBuilder
def create_schema_filter({:ids, ids}, query), do: where(query, [m], m.id in ^ids)
def create_schema_filter(query, :ids, ids), do: Query.where(query, [m], m.id in ^ids)

@impl QueryBuilder
def create_schema_filter({:offset, val}, query), do: offset(query, ^val)
def create_schema_filter(query, :offset, val), do: Query.offset(query, ^val)

@impl QueryBuilder
def create_schema_filter({:limit, val}, query), do: limit(query, ^val)
def create_schema_filter(query, :limit, val), do: Query.limit(query, ^val)

@impl QueryBuilder
def create_schema_filter({:first, val}, query), do: limit(query, ^val)
def create_schema_filter(query, :first, val), do: Query.limit(query, ^val)

@impl QueryBuilder
def create_schema_filter({:order_by, val}, query), do: order_by(query, ^val)
def create_schema_filter(query, :order_by, val), do: Query.order_by(query, ^val)

@impl QueryBuilder
def create_schema_filter({:last, val}, query) do
def create_schema_filter(query, :last, val) do
query
|> exclude(:order_by)
|> from(order_by: [desc: :inserted_at], limit: ^val)
|> subquery
|> order_by(:id)
|> Query.exclude(:order_by)
|> Query.from(order_by: [desc: :inserted_at], limit: ^val)
|> Query.subquery()
|> Query.order_by(:id)
end

@impl QueryBuilder
def create_schema_filter({:search, val}, query) do
schema = QueryBuilder.query_schema(query)
def create_schema_filter(query, :search, val) do
schema = QueryHelpers.get_queryable(query)

if function_exported?(schema, :by_search, 2) do
schema.by_search(query, val)
Expand Down
Loading

0 comments on commit 9653ea7

Please sign in to comment.