Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ ash-*.tar

# Ignoring Elixir Language Server
.elixir_ls/
.expert/

# Ignore Nix and direnv
shell.nix
Expand Down
2 changes: 1 addition & 1 deletion documentation/dsls/DSL-Ash.Resource.md
Original file line number Diff line number Diff line change
Expand Up @@ -3752,7 +3752,7 @@ end
| [`description`](#calculations-calculate-description){: #calculations-calculate-description } | `String.t` | | An optional description for the calculation |
| [`public?`](#calculations-calculate-public?){: #calculations-calculate-public? } | `boolean` | `false` | Whether or not the calculation will appear in public interfaces. |
| [`sensitive?`](#calculations-calculate-sensitive?){: #calculations-calculate-sensitive? } | `boolean` | `false` | Whether or not references to the calculation will be considered sensitive. |
| [`load`](#calculations-calculate-load){: #calculations-calculate-load } | `any` | `[]` | A load statement to be applied if the calculation is used. |
| [`load`](#calculations-calculate-load){: #calculations-calculate-load } | `any` | `[]` | A load statement to be applied if the calculation is used. Only works with module-based or function-based calculations, not expression calculations. |
| [`allow_nil?`](#calculations-calculate-allow_nil?){: #calculations-calculate-allow_nil? } | `boolean` | `true` | Whether or not the calculation can return nil. |
| [`filterable?`](#calculations-calculate-filterable?){: #calculations-calculate-filterable? } | `boolean \| :simple_equality` | `true` | Whether or not the calculation should be usable in filters. |
| [`sortable?`](#calculations-calculate-sortable?){: #calculations-calculate-sortable? } | `boolean` | `true` | Whether or not the calculation can be referenced in sorts. |
Expand Down
7 changes: 6 additions & 1 deletion lib/ash/resource/calculation/calculation.ex
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@ defmodule Ash.Resource.Calculation do
load: [
type: :any,
default: [],
doc: "A load statement to be applied if the calculation is used."
doc:
"A load statement to be applied if the calculation is used. Only works with module-based or function-based calculations, not expression calculations."
],
allow_nil?: [
type: :boolean,
Expand Down Expand Up @@ -188,6 +189,10 @@ defmodule Ash.Resource.Calculation do
@callback calculate(records :: [Ash.Resource.record()], opts :: opts, context :: Context.t()) ::
{:ok, [term]} | [term] | {:error, term} | :unknown
@callback expression(opts :: opts, context :: Context.t()) :: any
@doc """
A load statement to be applied when the calculation is used.
Only works with module-based or function-based calculations, not expression calculations.
"""
@callback load(query :: Ash.Query.t(), opts :: opts, context :: Context.t()) ::
atom | [atom] | Keyword.t()
@callback strict_loads?() :: boolean()
Expand Down
1 change: 1 addition & 0 deletions lib/ash/resource/dsl.ex
Original file line number Diff line number Diff line change
Expand Up @@ -1664,6 +1664,7 @@ defmodule Ash.Resource.Dsl do
@verifiers [
Ash.Resource.Verifiers.ValidateRelationshipAttributesMatch,
Ash.Resource.Verifiers.VerifyReservedCalculationArguments,
Ash.Resource.Verifiers.VerifyCalculations,
Ash.Resource.Verifiers.VerifyIdentityFields,
Ash.Resource.Verifiers.VerifyPrimaryReadActionHasNoArguments,
Ash.Resource.Verifiers.VerifySelectedByDefault,
Expand Down
56 changes: 56 additions & 0 deletions lib/ash/resource/verifiers/verify_calculations.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# SPDX-FileCopyrightText: 2019 ash contributors <https://github.com/ash-project/ash/graphs/contributors>
#
# SPDX-License-Identifier: MIT

defmodule Ash.Resource.Verifiers.VerifyCalculations do
@moduledoc """
Validates calculation configurations.

Currently checks:
- Warns when the `load` option is used with expression calculations
"""
use Spark.Dsl.Verifier

alias Spark.Error.DslError

def verify(dsl) do
module = Spark.Dsl.Verifier.get_persisted(dsl, :module)

warnings =
dsl
|> Ash.Resource.Info.calculations()
|> Enum.flat_map(fn calculation ->
verify_load_with_expression(calculation, module)
end)

{:warn, Enum.map(warnings, &Exception.message/1)}
end

defp verify_load_with_expression(calculation, module) do
is_expr_calc =
case calculation.calculation do
{Ash.Resource.Calculation.Expression, _} -> true
_ -> false
end

has_load = calculation.load != [] && calculation.load != nil

if is_expr_calc && has_load do
[
DslError.exception(
module: module,
message: """
The `load` option is used on expression calculation `#{calculation.name}`, \
but `load` only works with Elixir calculations (module-based or function-based).

Expression calculations automatically determine their dependencies. \
If you need to use the `load` option, convert this to an Elixir calculation module.
""",
path: [:calculations, calculation.name]
)
]
else
[]
end
end
end
89 changes: 89 additions & 0 deletions test/calculations/calculation_with_load_read_action_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# SPDX-FileCopyrightText: 2019 ash contributors <https://github.com/ash-project/ash/graphs.contributors>
#
# SPDX-License-Identifier: MIT

defmodule Ash.Test.CalculationWithLoadReadActionTest do
@moduledoc false
use ExUnit.Case, async: true

defmodule Related do
use Ash.Resource,
data_layer: Ash.DataLayer.Ets,
domain: Ash.Test.Domain

ets do
private? true
end

actions do
default_accept :*
defaults([:read, create: :*])

read :read_active do
filter expr(active == true)
end

read :read_all do
end
end

attributes do
uuid_primary_key(:id)
attribute(:name, :string, public?: true)
attribute(:active, :boolean, public?: true, default: false)
end

relationships do
belongs_to :parent, Ash.Test.CalculationWithLoadReadActionTest.Parent do
public? true
allow_nil? false
end
end
end

test "warns when load option is used with expression calculation" do
output =
ExUnit.CaptureIO.capture_io(:stderr, fn ->
defmodule ParentWithExprLoad do
use Ash.Resource,
data_layer: Ash.DataLayer.Ets,
domain: Ash.Test.Domain

ets do
private? true
end

actions do
default_accept :*
defaults([:read, create: :*])
end

attributes do
uuid_primary_key(:id)
end

calculations do
calculate :calculated_common_name,
:string,
expr(related.name),
load: [
related: Ash.Query.for_read(Related, :read_all)
]
end

relationships do
has_one :related, Related do
public? true
destination_attribute :parent_id
read_action :read_active
end
end
end
end)

assert output =~
"The `load` option is used on expression calculation `calculated_common_name`"

assert output =~ "but `load` only works with Elixir calculations"
end
end