Skip to content

Commit 26ca3dd

Browse files
Adding ability to sort by columns (#33)
* Adding ability to sort by columns * Add a few tests for the columns query * Bump version
1 parent c6dff98 commit 26ca3dd

File tree

6 files changed

+216
-61
lines changed

6 files changed

+216
-61
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,5 @@ npm-debug.log
2626
# variables.
2727
/config/*.secret.exs
2828

29-
doc/
29+
doc/
30+
.elixir_ls/

example/test/phoenix_datatables/query_test.exs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,64 @@ defmodule PhoenixDatatables.QueryTest do
280280

281281
end
282282

283+
describe "search_columns" do
284+
test "returns 1 result when 1 column matches" do
285+
add_items()
286+
287+
query =
288+
(from item in Item,
289+
join: category in assoc(item, :category),
290+
select: %{id: item.id, category_name: category.name, nsn: item.nsn})
291+
292+
request = Factory.raw_request()
293+
params = update_in(request, ["columns", "0", "search"], &(Map.put(&1, "value", "1NSN")))
294+
|> Request.receive()
295+
296+
results = Query.search_columns(query, params, [columns: [id: 0, category_name: 0, nsn: 0]])
297+
|> Repo.all()
298+
299+
assert Enum.count(results) == 1
300+
end
301+
302+
test "returns both results when they all match column searches" do
303+
add_items()
304+
305+
query =
306+
(from item in Item,
307+
join: category in assoc(item, :category),
308+
select: %{id: item.id, category_name: category.name, nsn: item.nsn})
309+
310+
request = Factory.raw_request()
311+
params = update_in(request, ["columns", "0", "search"], &(Map.put(&1, "value", "NSN")))
312+
|> Request.receive()
313+
314+
results = Query.search_columns(query, params, [columns: [id: 0, category_name: 0, nsn: 0]])
315+
|> Repo.all()
316+
317+
assert Enum.count(results) == 2
318+
end
319+
320+
test "returns no results when not all columns match" do
321+
add_items()
322+
323+
query =
324+
(from item in Item,
325+
join: category in assoc(item, :category),
326+
select: %{id: item.id, category_name: category.name, nsn: item.nsn, aac: item.aac})
327+
328+
request = Factory.raw_request()
329+
params = request
330+
|> update_in(["columns", "0", "search"], &(Map.put(&1, "value", "NSN")))
331+
|> update_in(["columns", "6", "search"], &(Map.put(&1, "value", "no match")))
332+
|> Request.receive()
333+
334+
results = Query.search_columns(query, params, [columns: [id: 0, category_name: 0, nsn: 0, aac: 0]])
335+
|> Repo.all()
336+
337+
assert Enum.empty?(results)
338+
end
339+
end
340+
283341
describe "total_entries" do
284342
test "returns number of results in specified schema" do
285343
add_items()

lib/phoenix_datatables.ex

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@ defmodule PhoenixDatatables do
44
by the `Repo.fetch_datatable` function and directly by client applications.
55
"""
66

7-
alias PhoenixDatatables.Request
87
alias PhoenixDatatables.Query
8+
alias PhoenixDatatables.Request
99
alias PhoenixDatatables.Response
1010
alias PhoenixDatatables.Response.Payload
11+
1112
alias Plug.Conn
1213

1314
@doc """
@@ -50,7 +51,6 @@ defmodule PhoenixDatatables do
5051
5152
 
5253
53-
5454
* `:total_entries` - Provides a way for the application to use cached values for total_entries; when this
5555
is provided, `phoenix_datatables` won't do a query to get the total record count, instead using
5656
the provided value in the response. The mechanism for cacheing is left up to the application.
@@ -81,6 +81,7 @@ defmodule PhoenixDatatables do
8181
query
8282
|> Query.sort(params, options)
8383
|> Query.search(params, options)
84+
|> Query.search_columns(params, options)
8485
|> Query.paginate(params)
8586

8687
filtered_entries =

lib/phoenix_datatables/query.ex

Lines changed: 110 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@ defmodule PhoenixDatatables.Query do
55
import Ecto.Query
66
use PhoenixDatatables.Query.Macros
77
alias Ecto.Query.JoinExpr
8-
alias PhoenixDatatables.Request.Params
9-
alias PhoenixDatatables.Request.Column
10-
alias PhoenixDatatables.Request.Search
118
alias PhoenixDatatables.Query.Attribute
129
alias PhoenixDatatables.QueryException
10+
alias PhoenixDatatables.Request.Column
11+
alias PhoenixDatatables.Request.Params
12+
alias PhoenixDatatables.Request.Search
1313

1414
@doc """
1515
Add order_by clauses to the provided queryable based on the "order" params provided
@@ -24,30 +24,32 @@ defmodule PhoenixDatatables.Query do
2424
else
2525
build_schema_sorts(queryable, params)
2626
end
27+
2728
do_sorts(queryable, sorts, options)
2829
end
2930

3031
defp build_column_sorts(%Params{order: orders} = params, columns) do
3132
for order <- orders do
3233
with dir when is_atom(dir) <- cast_dir(order.dir),
33-
%Column{} = column <- params.columns[order.column],
34-
true <- column.orderable,
35-
{column, join_index} when is_number(join_index)
36-
<- cast_column(column.data, columns) do
34+
%Column{} = column <- params.columns[order.column],
35+
true <- column.orderable,
36+
{column, join_index} when is_number(join_index) <-
37+
cast_column(column.data, columns) do
3738
{dir, column, join_index}
3839
end
3940
end
4041
end
4142

4243
defp build_schema_sorts(queryable, %Params{order: orders} = params) do
4344
schema = schema(queryable)
45+
4446
for order <- orders do
4547
with dir when is_atom(dir) <- cast_dir(order.dir),
46-
%Column{} = column <- params.columns[order.column],
47-
true <- column.orderable,
48-
%Attribute{} = attribute <- Attribute.extract(column.data, schema),
49-
join_index when is_number(join_index)
50-
<- join_order(queryable, attribute.parent) do
48+
%Column{} = column <- params.columns[order.column],
49+
true <- column.orderable,
50+
%Attribute{} = attribute <- Attribute.extract(column.data, schema),
51+
join_index when is_number(join_index) <-
52+
join_order(queryable, attribute.parent) do
5153
{dir, attribute.name, join_index}
5254
end
5355
end
@@ -61,28 +63,31 @@ defmodule PhoenixDatatables.Query do
6163

6264
@doc false
6365
def join_order(_, nil), do: 0
66+
6467
def join_order(%Ecto.Query{} = queryable, parent) do
6568
case Enum.find_index(queryable.joins, &(join_relation(&1) == parent)) do
6669
nil -> nil
6770
number when is_number(number) -> number + 1
6871
end
6972
end
73+
7074
def join_order(queryable, parent) do
7175
QueryException.raise(:join_order, """
7276
73-
An attempt was made to interrogate the join structure of #{inspect queryable}
77+
An attempt was made to interrogate the join structure of #{inspect(queryable)}
7478
This is not an %Ecto.Query{}. The most likely cause for this error is using
7579
dot-notation(e.g. 'category.name') in the column name defined in the datatables
7680
client config but a simple Schema (no join) is used as the underlying queryable.
7781
78-
Please check the client config for the fields belonging to #{inspect parent}. If
82+
Please check the client config for the fields belonging to #{inspect(parent)}. If
7983
the required field does belong to a different parent schema, that schema needs to
8084
be joined in the Ecto query.
8185
8286
""")
8387
end
8488

8589
defp join_relation(%JoinExpr{assoc: {_, relation}}), do: relation
90+
8691
defp join_relation(_) do
8792
QueryException.raise(:join_relation, """
8893
@@ -107,27 +112,36 @@ defmodule PhoenixDatatables.Query do
107112
108113
""")
109114
end
115+
110116
defp check_from(from), do: from
111117

112118
defp cast_column(column_name, sortable)
113-
when is_list(sortable)
114-
and is_tuple(hd(sortable))
115-
and is_atom(elem(hd(sortable), 0)) do #Keyword
119+
# Keyword
120+
when is_list(sortable) and
121+
is_tuple(hd(sortable)) and
122+
is_atom(elem(hd(sortable), 0)) do
116123
[parent | child] = String.split(column_name, ".")
124+
117125
if parent in Enum.map(Keyword.keys(sortable), &Atom.to_string/1) do
118126
member = Keyword.fetch!(sortable, String.to_atom(parent))
127+
119128
case member do
120129
children when is_list(children) ->
121130
with [child] <- child,
122-
[child] <- Enum.filter(Keyword.keys(children),
123-
&(Atom.to_string(&1) == child)),
124-
{:ok, order} when is_number(order)
125-
<- Keyword.fetch(children, child) do
131+
[child] <-
132+
Enum.filter(
133+
Keyword.keys(children),
134+
&(Atom.to_string(&1) == child)
135+
),
136+
{:ok, order} when is_number(order) <-
137+
Keyword.fetch(children, child) do
126138
{child, order}
127139
else
128140
_ -> {:error, "#{column_name} is not a sortable column."}
129141
end
130-
order when is_number(order) -> {String.to_atom(parent), order}
142+
143+
order when is_number(order) ->
144+
{String.to_atom(parent), order}
131145
end
132146
else
133147
{:error, "#{column_name} is not a sortable column."}
@@ -166,7 +180,9 @@ defmodule PhoenixDatatables.Query do
166180
true ->
167181
{num, _} = Integer.parse(num)
168182
num
169-
false -> num
183+
184+
false ->
185+
num
170186
end
171187
end
172188

@@ -180,41 +196,91 @@ defmodule PhoenixDatatables.Query do
180196
columns = options[:columns]
181197
do_search(queryable, params, columns)
182198
end
199+
183200
defp do_search(queryable, %Params{search: %Search{value: ""}}, _), do: queryable
201+
184202
defp do_search(queryable, %Params{} = params, searchable) when is_list(searchable) do
185203
search_term = "%#{params.search.value}%"
186204
dynamic = dynamic([], false)
187-
dynamic = Enum.reduce params.columns, dynamic, fn({_, v}, acc_dynamic) ->
188-
with {column, join_index} when is_number(join_index)
189-
<- v.data |> cast_column(searchable),
190-
true <- v.searchable do
191-
acc_dynamic
192-
|> search_relation(join_index,
193-
column,
194-
search_term)
195-
else
196-
_ -> acc_dynamic
197-
end
198-
end
205+
206+
dynamic =
207+
Enum.reduce(params.columns, dynamic, fn {_, v}, acc_dynamic ->
208+
with {column, join_index} when is_number(join_index) <-
209+
v.data |> cast_column(searchable),
210+
true <- v.searchable do
211+
acc_dynamic
212+
|> search_relation(
213+
join_index,
214+
column,
215+
search_term
216+
)
217+
else
218+
_ -> acc_dynamic
219+
end
220+
end)
221+
199222
where(queryable, [], ^dynamic)
200223
end
201224

202225
defp do_search(queryable, %Params{search: search, columns: columns}, _searchable) do
203226
search_term = "%#{search.value}%"
204227
schema = schema(queryable)
205228
dynamic = dynamic([], false)
229+
206230
dynamic =
207-
Enum.reduce columns, dynamic, fn({_, v}, acc_dynamic) ->
231+
Enum.reduce(columns, dynamic, fn {_, v}, acc_dynamic ->
208232
with %Attribute{} = attribute <- v.data |> Attribute.extract(schema),
209-
true <- v.searchable do
233+
true <- v.searchable do
210234
acc_dynamic
211-
|> search_relation(join_order(queryable, attribute.parent),
212-
attribute.name,
213-
search_term)
235+
|> search_relation(
236+
join_order(queryable, attribute.parent),
237+
attribute.name,
238+
search_term
239+
)
214240
else
215241
_ -> acc_dynamic
216242
end
217-
end
243+
end)
244+
245+
where(queryable, [], ^dynamic)
246+
end
247+
248+
def search_columns(queryable, params, options \\ []) do
249+
if has_column_search?(params.columns) do
250+
columns = options[:columns] || []
251+
do_search_columns(queryable, params, columns)
252+
else
253+
queryable
254+
end
255+
end
256+
257+
defp has_column_search?(columns) when is_map(columns) do
258+
columns = Map.values(columns)
259+
Enum.any?(columns, &(&1.search.value != ""))
260+
end
261+
262+
defp has_column_search?(_), do: false
263+
264+
defp do_search_columns(queryable, params, columns) do
265+
dynamic = dynamic([], true)
266+
267+
dynamic =
268+
Enum.reduce(params.columns, dynamic, fn {_, v}, acc_dynamic ->
269+
with {column, join_index} when is_number(join_index) <-
270+
cast_column(v.data, columns),
271+
true <- v.searchable,
272+
true <- v.search.value != "" do
273+
acc_dynamic
274+
|> search_relation_and(
275+
join_index,
276+
column,
277+
"%#{v.search.value}%"
278+
)
279+
else
280+
_ -> acc_dynamic
281+
end
282+
end)
283+
218284
where(queryable, [], ^dynamic)
219285
end
220286

@@ -239,15 +305,15 @@ defmodule PhoenixDatatables.Query do
239305

240306
total_entries || 0
241307
end
242-
243308
end
244309

245310
defmodule PhoenixDatatables.QueryException do
246311
defexception [:message, :operation]
247312

248-
@dialyzer {:no_return, raise: 1} #yes we know it raises
313+
# yes we know it raises
314+
@dialyzer {:no_return, raise: 1}
249315

250316
def raise(operation, message \\ "") do
251-
Kernel.raise __MODULE__, [operation: operation, message: message]
317+
Kernel.raise(__MODULE__, operation: operation, message: message)
252318
end
253319
end

0 commit comments

Comments
 (0)