Skip to content

Commit aeb819c

Browse files
authored
fix: Don't throw exceptions raised based on missing context while validating expr refs (#2826)
1 parent 49f38b4 commit aeb819c

3 files changed

Lines changed: 118 additions & 102 deletions

File tree

lib/ash/actions/read/read.ex

Lines changed: 22 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1944,49 +1944,22 @@ defmodule Ash.Actions.Read do
19441944
end)
19451945
|> Enum.reduce_while({:ok, []}, fn
19461946
{%Ash.Resource.Calculation{} = resource_calculation, direction}, {:ok, sort} ->
1947-
case Ash.Query.Calculation.from_resource_calculation(
1948-
query.resource,
1949-
resource_calculation,
1950-
source_context: query.context
1951-
) do
1952-
{:ok, calc} ->
1953-
case hydrate_calculations(query, [calc]) do
1954-
{:ok, [{calc, expression}]} ->
1955-
{:cont,
1956-
{:ok,
1957-
[
1958-
{%{
1959-
calc
1960-
| module: Ash.Resource.Calculation.Expression,
1961-
opts: [expr: expression]
1962-
}, direction}
1963-
| sort
1964-
]}}
1965-
1966-
{:error, error} ->
1967-
{:halt, {:error, error}}
1968-
end
1969-
1970-
{:error, error} ->
1971-
{:halt, {:error, error}}
1947+
with {:ok, calc} <-
1948+
Ash.Query.Calculation.from_resource_calculation(
1949+
query.resource,
1950+
resource_calculation,
1951+
source_context: query.context
1952+
),
1953+
{:ok, entry} <- hydrate_sort_calculation(query, calc, direction) do
1954+
{:cont, {:ok, [entry | sort]}}
1955+
else
1956+
{:error, error} -> {:halt, {:error, error}}
19721957
end
19731958

19741959
{%Ash.Query.Calculation{} = calc, direction}, {:ok, sort} ->
1975-
case hydrate_calculations(query, [calc]) do
1976-
{:ok, [{calc, expression}]} ->
1977-
{:cont,
1978-
{:ok,
1979-
[
1980-
{%{
1981-
calc
1982-
| module: Ash.Resource.Calculation.Expression,
1983-
opts: [expr: expression]
1984-
}, direction}
1985-
| sort
1986-
]}}
1987-
1988-
{:error, error} ->
1989-
{:halt, {:error, error}}
1960+
case hydrate_sort_calculation(query, calc, direction) do
1961+
{:ok, entry} -> {:cont, {:ok, [entry | sort]}}
1962+
{:error, error} -> {:halt, {:error, error}}
19901963
end
19911964

19921965
{%Ash.Resource.Aggregate{} = agg, direction}, {:ok, sort} ->
@@ -2035,6 +2008,15 @@ defmodule Ash.Actions.Read do
20352008
end)
20362009
end
20372010

2011+
defp hydrate_sort_calculation(query, calc, direction) do
2012+
with {:ok, [{calc, expression}]} <- hydrate_calculations(query, [calc]),
2013+
:ok <- Ash.Sort.validate_expression_refs(query.resource, expression) do
2014+
{:ok,
2015+
{%{calc | module: Ash.Resource.Calculation.Expression, opts: [expr: expression]},
2016+
direction}}
2017+
end
2018+
end
2019+
20382020
defp hydrate_aggregates(query) do
20392021
Enum.reduce_while(query.aggregates, {:ok, %{}}, fn {key, aggregate}, {:ok, aggregates} ->
20402022
aggregate = %{

lib/ash/sort/sort.ex

Lines changed: 13 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -385,44 +385,25 @@ defmodule Ash.Sort do
385385
end
386386

387387
defp validate_sortable(resource, field) do
388-
with :ok <- validate_expression_refs(resource, field) do
389-
if type_sortable?(resource, field) do
390-
:ok
391-
else
392-
{:error, UnsortableField.exception(field: field_name(field), resource: resource)}
393-
end
388+
if type_sortable?(resource, field) do
389+
:ok
390+
else
391+
{:error, UnsortableField.exception(field: field_name(field), resource: resource)}
394392
end
395393
end
396394

397-
defp validate_expression_refs(
398-
resource,
399-
%Ash.Query.Calculation{module: module, opts: opts, context: context}
400-
) do
401-
if Ash.Resource.Calculation.has_expression?(module) do
402-
module
403-
|> Ash.Resource.Calculation.expression(opts, context)
404-
|> Ash.Filter.hydrate_refs(%{resource: resource, public?: false})
405-
|> case do
406-
{:ok, expression} ->
407-
expression
408-
|> Ash.Filter.list_refs(false, false, true)
409-
|> Enum.reduce_while(:ok, fn ref, :ok ->
410-
case validate_expression_ref(resource, ref) do
411-
:ok -> {:cont, :ok}
412-
{:error, error} -> {:halt, {:error, error}}
413-
end
414-
end)
415-
416-
{:error, error} ->
417-
{:error, error}
395+
@doc false
396+
def validate_expression_refs(resource, expression) do
397+
expression
398+
|> Ash.Filter.list_refs(false, false, true)
399+
|> Enum.reduce_while(:ok, fn ref, :ok ->
400+
case validate_expression_ref(resource, ref) do
401+
:ok -> {:cont, :ok}
402+
{:error, error} -> {:halt, {:error, error}}
418403
end
419-
else
420-
:ok
421-
end
404+
end)
422405
end
423406

424-
defp validate_expression_refs(_resource, _field), do: :ok
425-
426407
defp validate_expression_ref(
427408
resource,
428409
%Ash.Query.Ref{attribute: field, relationship_path: relationship_path}

test/sort/sort_test.exs

Lines changed: 83 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,10 @@ defmodule Ash.Test.Sort.SortTest do
7070
calculate :title_calculation, :string, expr(title),
7171
public?: true,
7272
sortable?: false
73+
74+
calculate :context_dependent, :string, Ash.Test.Sort.SortTest.ContextDependent do
75+
public? true
76+
end
7377
end
7478

7579
relationships do
@@ -124,6 +128,19 @@ defmodule Ash.Test.Sort.SortTest do
124128
end
125129
end
126130

131+
defmodule ContextDependent do
132+
@moduledoc false
133+
use Ash.Resource.Calculation
134+
135+
@impl true
136+
def expression(_opts, context) do
137+
case context.source_context[:sort_field] do
138+
nil -> raise "requires :sort_field in the source context"
139+
field -> expr(^ref(field))
140+
end
141+
end
142+
end
143+
127144
defmodule NoSortDataLayer do
128145
use Spark.Dsl.Extension, sections: []
129146

@@ -189,6 +206,33 @@ defmodule Ash.Test.Sort.SortTest do
189206
end
190207
end
191208

209+
describe "sorting on calculations whose expression/2 reads context.source_context" do
210+
test "does not raise when the query has no context set" do
211+
assert %Ash.Query{valid?: true} = Ash.Query.sort(Post, context_dependent: :asc)
212+
end
213+
214+
test "does not raise when the query context was set before sorting" do
215+
assert %Ash.Query{valid?: true} =
216+
Post
217+
|> Ash.Query.set_context(%{sort_field: :title})
218+
|> Ash.Query.sort(context_dependent: :asc)
219+
end
220+
221+
test "reads successfully when the context is set" do
222+
b = Post |> Ash.Changeset.for_create(:create, %{title: "b"}) |> Ash.create!()
223+
a = Post |> Ash.Changeset.for_create(:create, %{title: "a"}) |> Ash.create!()
224+
225+
ids =
226+
Post
227+
|> Ash.Query.set_context(%{sort_field: :title})
228+
|> Ash.Query.sort(context_dependent: :asc)
229+
|> Ash.read!()
230+
|> Enum.map(& &1.id)
231+
232+
assert ids == [a.id, b.id]
233+
end
234+
end
235+
192236
describe "parse_input/2" do
193237
test "simple string sort parses properly" do
194238
assert {:ok, [title: :asc, contents: :desc]} =
@@ -333,38 +377,47 @@ defmodule Ash.Test.Sort.SortTest do
333377
end
334378
end
335379

336-
test "expression sorts reject references to unsortable fields and relationships" do
380+
test "expression sorts reject references to unsortable fields and relationships at read time" do
337381
require Ash.Sort
338382

339-
assert %Ash.Query{
340-
valid?: false,
341-
errors: [
342-
%Ash.Error.Query.UnsortableField{
343-
resource: Ash.Test.Sort.SortTest.Post,
344-
field: :unsortable_title
345-
}
346-
]
347-
} = Ash.Query.sort(Post, Ash.Sort.expr_sort(unsortable_title, :string))
348-
349-
assert %Ash.Query{
350-
valid?: false,
351-
errors: [
352-
%Ash.Error.Query.UnsortableField{
353-
resource: Ash.Test.Sort.SortTest.Author,
354-
field: :unsortable_name
355-
}
356-
]
357-
} = Ash.Query.sort(Post, Ash.Sort.expr_sort(author.unsortable_name, :string))
358-
359-
assert %Ash.Query{
360-
valid?: false,
361-
errors: [
362-
%Ash.Error.Query.UnsortableField{
363-
resource: Ash.Test.Sort.SortTest.Post,
364-
field: :unsortable_author
365-
}
366-
]
367-
} = Ash.Query.sort(Post, Ash.Sort.expr_sort(unsortable_author.name, :string))
383+
assert {:error,
384+
%Ash.Error.Invalid{
385+
errors: [
386+
%Ash.Error.Query.UnsortableField{
387+
resource: Ash.Test.Sort.SortTest.Post,
388+
field: :unsortable_title
389+
}
390+
]
391+
}} =
392+
Post
393+
|> Ash.Query.sort(Ash.Sort.expr_sort(unsortable_title, :string))
394+
|> Ash.read()
395+
396+
assert {:error,
397+
%Ash.Error.Invalid{
398+
errors: [
399+
%Ash.Error.Query.UnsortableField{
400+
resource: Ash.Test.Sort.SortTest.Author,
401+
field: :unsortable_name
402+
}
403+
]
404+
}} =
405+
Post
406+
|> Ash.Query.sort(Ash.Sort.expr_sort(author.unsortable_name, :string))
407+
|> Ash.read()
408+
409+
assert {:error,
410+
%Ash.Error.Invalid{
411+
errors: [
412+
%Ash.Error.Query.UnsortableField{
413+
resource: Ash.Test.Sort.SortTest.Post,
414+
field: :unsortable_author
415+
}
416+
]
417+
}} =
418+
Post
419+
|> Ash.Query.sort(Ash.Sort.expr_sort(unsortable_author.name, :string))
420+
|> Ash.read()
368421
end
369422

370423
test "nested sorts enforce field and relationship flags" do

0 commit comments

Comments
 (0)