Skip to content

Commit

Permalink
support and && or symbol condition && assoc table search.
Browse files Browse the repository at this point in the history
  • Loading branch information
zven21 committed Aug 11, 2018
1 parent 4ed0ab3 commit fca71ef
Show file tree
Hide file tree
Showing 6 changed files with 182 additions and 71 deletions.
8 changes: 7 additions & 1 deletion .iex.exs
@@ -1,4 +1,10 @@


import Ecto.Query

alias Turbo.Ecto.Services.BuildSearchQuery
alias Turbo.Ecto.Hooks.Search
alias Turbo.Ecto.Hooks.Sort
alias Turbo.Ecto.Hooks.Paginate
alias Turbo.Ecto.Hooks.Paginate

alias Turbo.Ecto.{Category, Product, Variant}
2 changes: 1 addition & 1 deletion README.md
Expand Up @@ -22,7 +22,7 @@ Phoenix support `turbo_html`, check [this](https://github.com/zven21/turbo_html)
```elixir
def deps do
[
{:turbo_ecto, "~> 0.1.3"}
{:turbo_ecto, "~> 0.1.5"}
]
end
```
Expand Down
55 changes: 51 additions & 4 deletions lib/turbo_ecto.ex
@@ -1,6 +1,52 @@
defmodule Turbo.Ecto do
@moduledoc """
A rich ecto component, including search sort and paginate. https://hexdocs.pm/turbo_ecto
## Example
### Category Table Structure
| Field | Type | Comment |
| ------------- | ------------- | --------- |
| `name` | string | |
### Product Table Structure
| Field | Type | Comment |
| ------------- | ------------- | --------- |
| `title` | string | |
| `body` | text | |
| `price` | float | |
| `category_id` | integer | |
| `available` | boolean | |
### Variant Table Structure
| Field | Type | Comment |
| ------------- | ------------- | --------- |
| `title` | string | |
| `price` | float | |
| `product_id` | integer | |
* Input Search
```elixir
url_query = http://localhost:4000/varinats?q[product_category_name_and_product_name_or_name_like]=elixir
```
* Expect output:
```elixir
iex> params = %{"q" => %{"product_category_name_and_product_name_or_name_like" => "elixir"}}
iex> Turbo.Ecto.turboq(Turbo.Ecto.Variant, params)
#Ecto.Query<from v in subquery(from v in subquery(from v in subquery(from v in Turbo.Ecto.Variant),
or_where: like(v.name, ^"%elixir%")),
join: p in assoc(v, :product),
join: c in assoc(p, :category),
where: like(c.name, ^"%elixir%")), join: p in assoc(v, :product), where: like(p.name, ^"%elixir%"), limit: ^10, offset: ^0>
```
"""

alias Turbo.Ecto.Hooks.{Paginate, Sort, Search}
Expand Down Expand Up @@ -36,9 +82,10 @@ defmodule Turbo.Ecto do
## Example
iex> params = %{"q" => %{"name_like" => "name", "body_like" => "body"}, "s" => "updated_at+asc", "per_page" => 5, "page" => 1}
iex> params = %{"q" => %{"name_or_body_like" => "elixir"}, "s" => "updated_at+asc", "per_page" => 5, "page" => 1}
iex> Turbo.Ecto.turboq(Turbo.Ecto.Product, params)
#Ecto.Query<from p in Turbo.Ecto.Product, where: like(p.body, ^"%body%"), where: like(p.name, ^"%name%"), order_by: [asc: p.updated_at], limit: ^5, offset: ^0>
#Ecto.Query<from p in subquery(from p in subquery(from p in Turbo.Ecto.Product),
or_where: like(p.body, ^"%elixir%")), where: like(p.name, ^"%elixir%"), order_by: [asc: p.updated_at], limit: ^5, offset: ^0>
"""
@spec turboq(Ecto.Query.t(), Map.t()) :: Ecto.Query.t()
Expand Down Expand Up @@ -69,8 +116,8 @@ defmodule Turbo.Ecto do
## Example
iex> Turbo.Ecto.searchq(Turbo.Ecto.Product, %{"q" => %{"name_like" => "q"}})
#Ecto.Query<from p in Turbo.Ecto.Product, where: like(p.name, ^"%q%")>
iex> Turbo.Ecto.searchq(Turbo.Ecto.Product, %{"q" => %{"name_like" => "elixir"}})
#Ecto.Query<from p in subquery(from p in Turbo.Ecto.Product), where: like(p.name, ^"%elixir%")>
"""
@spec search(Ecto.Query.t(), Map.t()) :: Ecto.Query.t()
Expand Down
182 changes: 118 additions & 64 deletions lib/turbo_ecto/hooks/search.ex
@@ -1,8 +1,11 @@
defmodule Turbo.Ecto.Hooks.Search do
@moduledoc """
Single Table Search
This module provides a operations that can add searching functionality to
a pipeline of `Ecto` queries. This module works by taking fields.
"""

import Ecto.Query

alias Turbo.Ecto.Services.BuildSearchQuery
alias Turbo.Ecto.Utils

Expand All @@ -12,47 +15,86 @@ defmodule Turbo.Ecto.Hooks.Search do
## Example
When build params use `:like`
When search_type is `:like`
iex> params = %{"q" => %{"name_like" => "elixir"}}
iex> Turbo.Ecto.Hooks.Search.run(Turbo.Ecto.Product, params)
#Ecto.Query<from p in Turbo.Ecto.Product, where: like(p.name, ^"%elixir%")>
#Ecto.Query<from p in subquery(from p in Turbo.Ecto.Product), where: like(p.name, ^"%elixir%")>
When params include `:ilike`
When search_type is `:ilike`
iex> params = %{"q" => %{"name_ilike" => "elixir"}}
iex> Turbo.Ecto.Hooks.Search.run(Turbo.Ecto.Product, params)
#Ecto.Query<from p in Turbo.Ecto.Product, where: ilike(p.name, ^"%elixir%")>
#Ecto.Query<from p in subquery(from p in Turbo.Ecto.Product), where: ilike(p.name, ^"%elixir%")>
When params include `:eq`
When search_type is `:eq`
iex> params = %{"q" => %{"price_eq" => 100}}
iex> Turbo.Ecto.Hooks.Search.run(Turbo.Ecto.Product, params)
#Ecto.Query<from p in Turbo.Ecto.Product, where: p.price == ^100>
#Ecto.Query<from p in subquery(from p in Turbo.Ecto.Product), where: p.price == ^100>
When params include `:gt`
When search_type is `:gt`
iex> params = %{"q" => %{"price_gt" => 100}}
iex> Turbo.Ecto.Hooks.Search.run(Turbo.Ecto.Product, params)
#Ecto.Query<from p in Turbo.Ecto.Product, where: p.price > ^100>
#Ecto.Query<from p in subquery(from p in Turbo.Ecto.Product), where: p.price > ^100>
When params include `:lt`
When search_type is `:lt`
iex> params = %{"q" => %{"price_lt" => 100}}
iex> Turbo.Ecto.Hooks.Search.run(Turbo.Ecto.Product, params)
#Ecto.Query<from p in Turbo.Ecto.Product, where: p.price < ^100>
#Ecto.Query<from p in subquery(from p in Turbo.Ecto.Product), where: p.price < ^100>
When params include `:gteq`
When search_type is `:gteq`
iex> params = %{"q" => %{"price_gteq" => 100}}
iex> Turbo.Ecto.Hooks.Search.run(Turbo.Ecto.Product, params)
#Ecto.Query<from p in Turbo.Ecto.Product, where: p.price >= ^100>
#Ecto.Query<from p in subquery(from p in Turbo.Ecto.Product), where: p.price >= ^100>
When params include `:lteq`
When search_type is `:lteq`
iex> params = %{"q" => %{"price_lteq" => 100}}
iex> Turbo.Ecto.Hooks.Search.run(Turbo.Ecto.Product, params)
#Ecto.Query<from p in Turbo.Ecto.Product, where: p.price <= ^100>
#Ecto.Query<from p in subquery(from p in Turbo.Ecto.Product), where: p.price <= ^100>
when use `and` symbol condition
iex> params = %{"q" => %{"name_and_body_like" => "elixir"}}
iex> Turbo.Ecto.Hooks.Search.run(Turbo.Ecto.Product, params)
#Ecto.Query<from p in subquery(from p in subquery(from p in Turbo.Ecto.Product),
where: like(p.body, ^"%elixir%")), where: like(p.name, ^"%elixir%")>
when use `or` symbol condition
iex> params = %{"q" => %{"name_or_body_like" => "elixir"}}
iex> Turbo.Ecto.Hooks.Search.run(Turbo.Ecto.Product, params)
#Ecto.Query<from p in subquery(from p in subquery(from p in Turbo.Ecto.Product),
or_where: like(p.body, ^"%elixir%")), where: like(p.name, ^"%elixir%")>
when use `assoc`
iex> params = %{"q" => %{"category_name_like" => "elixir"}}
iex> Turbo.Ecto.Hooks.Search.run(Turbo.Ecto.Product, params)
#Ecto.Query<from p in subquery(from p in Turbo.Ecto.Product), join: c in assoc(p, :category), where: like(c.name, ^"%elixir%")>
when use `and` && `or` && `assoc` condition
iex> params = %{"q" => %{"category_name_or_name_and_body_like" => "elixir"}}
iex> Turbo.Ecto.Hooks.Search.run(Turbo.Ecto.Product, params)
#Ecto.Query<from p in subquery(from p in subquery(from p in subquery(from p in Turbo.Ecto.Product),
where: like(p.body, ^"%elixir%")),
join: c in assoc(p, :category),
where: like(c.name, ^"%elixir%")), or_where: like(p.name, ^"%elixir%")>
when multi association && `or` && `and` condition
iex> params = %{"q" => %{"product_category_name_and_product_name_or_name_like" => "elixir"}}
iex> Turbo.Ecto.Hooks.Search.run(Turbo.Ecto.Variant, params)
#Ecto.Query<from v in subquery(from v in subquery(from v in subquery(from v in Turbo.Ecto.Variant),
or_where: like(v.name, ^"%elixir%")),
join: p in assoc(v, :product),
join: c in assoc(p, :category),
where: like(c.name, ^"%elixir%")), join: p in assoc(v, :product), where: like(p.name, ^"%elixir%")>
"""
@spec run(Ecto.Query.t(), Map.t()) :: Ecto.Query.t()
Expand All @@ -61,84 +103,96 @@ defmodule Turbo.Ecto.Hooks.Search do
defp handle_search(queryable, search_params) do
search_params
|> Map.get("q", %{})
|> Enum.reduce(%{}, &build_query_map(&1, &2, queryable))
|> Enum.reduce(%{}, &build_search_mapbox(&1, &2, queryable))
|> Enum.reduce(queryable, &search_queryable(&1, &2))
end

# generate search map() at params.
# params = %{"q" => %{"name_or_category_name_like" => "elixir"}}
# Turbo.Ecto.Hooks.Search.run(Turbo.Ecto.Product, params)
def build_query_map({search_field_and_type, search_term}, search_map, queryable) do
# FIXME
search_regex = ~r/([a-z1-9_]+)_(like|ilike|eq|gt|lt|gteq|lteq)$/
# Generate search mapbox from search params.
def build_search_mapbox({search_field_and_type, search_term}, mapbox, queryable) do
search_regex = ~r/([a-z1-9_]+)_(#{decorator_search_types()})$/

if Regex.match?(search_regex, search_field_and_type) do
[_, match, search_type] = Regex.run(search_regex, search_field_and_type)

match
|> build_and_condition()
|> Enum.reduce(%{}, &build_or_condition(&1, &2))
|> Enum.reduce(search_map, &build_assoc_query(&1, &2, search_term, search_type, queryable))
|> Enum.reduce(mapbox, &build_assoc_query(&1, &2, search_term, search_type, queryable))
else
raise "Pls use valid search expr."
raise "Unknown search matchers, #{inspect search_field_and_type}\n" <>
"it should be endwith one of #{inspect decorator_search_types()}, click: https://github.com/zven21/turbo_ecto#search-matchers"
end
end

defp build_assoc_query({search_field, search_expr}, search_map, search_term, search_type, queryable) do
build_assco(search_field, search_field, search_type, queryable, [], search_map, search_type, search_term, search_expr)
# Build with `and` expr condition from search params.
defp build_and_condition(field) do
field
|> String.split(~r{(_and_)})
|> Enum.reduce(%{}, &(Map.put(&2, &1, :where)))
end

# Build with `or` expr cnndition from search params.
defp build_or_condition({search_field, _}, search_mapbox) do
[hd | tl] = String.split(search_field, ~r{(_or_)})

tl
|> Enum.reduce(search_mapbox, &(Map.put(&2, &1, :or_where)))
|> Map.put(hd, :where)
end

defp build_assco(search_field, field, search_type, queryable, assoc, search_map, search_type, search_term, search_expr) do
# Build with assoc tables.
defp build_assoc_query({search_field, search_expr}, mapbox, search_term, search_type, queryable),
do: do_build_assoc_query(search_field, search_field, search_type, search_term, search_expr, queryable, mapbox)

defp do_build_assoc_query(search_field, field, search_type, search_term, search_expr, queryable, mapbox, assoc \\ []) do
# Returns string of the queryable's associations.
assoc_tables = Enum.join(Utils.schema_from_query(queryable).__schema__(:associations), "|")

association_regex = ~r{^(#{assoc_tables})}
split_regex = ~r/^(#{assoc_tables})_([a-z_]+)/

cond do
String.to_atom(search_field) in Utils.schema_from_query(queryable).__schema__(:fields) ->
Map.put(search_map, field, %{
search_field: search_field,
search_term: search_term,
search_type: search_type,
search_expr: search_expr,
assocs: assoc
})
Map.put(mapbox, field,
%{
assoc: assoc,
search_expr: search_expr,
search_term: search_term,
search_field: String.to_atom(search_field),
search_type: String.to_atom(search_type)
}
)

Regex.match?(association_regex, search_field) ->
[_, table, search_field] = Regex.run(split_regex, search_field)
schema = Utils.schema_from_query(queryable).__schema__(:association, String.to_atom(table)).related
# FIXME
assoc = Enum.into(assoc, table)
build_assco(search_field, field, search_type, schema, assoc, search_map, search_type, search_term, search_expr)

true -> search_map
[_, assoc_table, search_field] = Regex.run(split_regex, search_field)
assoc_table = String.to_atom(assoc_table)
assoc_queryable = Utils.schema_from_query(queryable).__schema__(:association, assoc_table).related
assoc = assoc ++ [assoc_table]
# `search_field` may be in the assco table
do_build_assoc_query(search_field, field, search_type, search_term, search_expr, assoc_queryable, mapbox, assoc)

true -> mapbox
end
end

# Build with `and` expr condition
defp build_and_condition(field) do
field
|> String.split(~r{(_and_)})
|> Enum.reduce(%{}, &(Map.put(&2, &1, "where")))
end

# Build with `or` expr cnndition
defp build_or_condition({search_field, _}, search_ory) do
[hd | tl] = String.split(search_field, ~r{(_or_)})

tl
|> Enum.reduce(search_ory, &(Map.put(&2, &1, "or_where")))
|> Map.put(hd, "where")
end

# Generate search queryable.
defp search_queryable({_, map}, queryable) do
# FIXME
# assocs = Map.get(map, :assoc)
search_field = String.to_atom(Map.get(map, :search_field))
search_type = String.to_atom(Map.get(map, :search_type))
assoc = Map.get(map, :assoc)
search_field = Map.get(map, :search_field)
search_type = Map.get(map, :search_type)
search_term = Map.get(map, :search_term)
search_expr = String.to_atom(Map.get(map, :search_expr, :where))
# FIXME
BuildSearchQuery.run(queryable, search_field, {search_expr, search_type}, search_term)
search_expr = Map.get(map, :search_expr, :where)

assoc
|> Enum.reduce(from(e in subquery(queryable)), &join_by_assoc(&1, &2))
|> BuildSearchQuery.run(search_field, {search_expr, search_type}, search_term)
end
defp search_queryable(_, queryable), do: queryable

# Helper function which handles associations in a query with a join type.
defp join_by_assoc(assoc, queryable) do
join(queryable, :inner, [..., p1], p2 in assoc(p1, ^assoc))
end

defp decorator_search_types, do: BuildSearchQuery.search_types |> Enum.join("|")
end
4 changes: 4 additions & 0 deletions lib/turbo_ecto/services/build_search_query.ex
Expand Up @@ -56,6 +56,10 @@ defmodule Turbo.Ecto.Services.BuildSearchQuery do
"search_expr should be one of #{inspect @search_exprs}"
end

def search_types, do: @search_types
def search_exprs, do: @search_exprs


@doc """
Builds a searched `queryable` on top of the given `queryable` using
`field`, `search_term` and `search_expr` when the `search_type` is `like`.
Expand Down
2 changes: 1 addition & 1 deletion mix.exs
@@ -1,7 +1,7 @@
defmodule Turbo.Ecto.MixProject do
use Mix.Project

@version "0.1.4"
@version "0.1.5"
@github "https://github.com/zven21/turbo_ecto"

def project do
Expand Down

0 comments on commit fca71ef

Please sign in to comment.