From f10e92fc6be5065951e922b61872100976fcb5c7 Mon Sep 17 00:00:00 2001 From: Zach Daniel Date: Mon, 13 Jan 2025 12:47:52 -0500 Subject: [PATCH 01/22] improvement: warn when domain policies would be ignored by resources improvement: allow policy authorizer to be in authorizers key in domains --- lib/ash/domain/domain.ex | 6 +++ lib/ash/policy/authorizer/authorizer.ex | 3 +- .../authorizer/verifiers/verify_resources.ex | 44 +++++++++++++++++++ lib/ash/reactor/dsl/action_transformer.ex | 2 +- test/support/policy_rbac/domain.ex | 2 +- 5 files changed, 54 insertions(+), 3 deletions(-) create mode 100644 lib/ash/policy/authorizer/verifiers/verify_resources.ex diff --git a/lib/ash/domain/domain.ex b/lib/ash/domain/domain.ex index 6dad8061b..1f97e185d 100644 --- a/lib/ash/domain/domain.ex +++ b/lib/ash/domain/domain.ex @@ -22,6 +22,12 @@ defmodule Ash.Domain do use Spark.Dsl, default_extensions: [extensions: [Ash.Domain.Dsl]], + many_extension_kinds: [ + :authorizers + ], + extension_kind_types: [ + authorizers: {:wrap_list, {:behaviour, Ash.Authorizer}} + ], opt_schema: [ validate_config_inclusion?: [ type: :boolean, diff --git a/lib/ash/policy/authorizer/authorizer.ex b/lib/ash/policy/authorizer/authorizer.ex index ca9668a89..29377ee00 100644 --- a/lib/ash/policy/authorizer/authorizer.ex +++ b/lib/ash/policy/authorizer/authorizer.ex @@ -473,7 +473,8 @@ defmodule Ash.Policy.Authorizer do @verifiers [ Ash.Policy.Authorizer.Verifiers.VerifyInAuthorizers, - Ash.Policy.Authorizer.Verifiers.VerifySatSolverImplementation + Ash.Policy.Authorizer.Verifiers.VerifySatSolverImplementation, + Ash.Policy.Authorizer.Verifiers.VerifyResources ] use Spark.Dsl.Extension, diff --git a/lib/ash/policy/authorizer/verifiers/verify_resources.ex b/lib/ash/policy/authorizer/verifiers/verify_resources.ex new file mode 100644 index 000000000..abdfcd9ef --- /dev/null +++ b/lib/ash/policy/authorizer/verifiers/verify_resources.ex @@ -0,0 +1,44 @@ +defmodule Ash.Policy.Authorizer.Verifiers.VerifyResources do + @moduledoc false + use Spark.Dsl.Verifier + + def verify(dsl) do + dsl + |> Ash.Domain.Info.resources() + |> Enum.reject(fn resource -> + Ash.Policy.Authorizer in Ash.Resource.Info.authorizers(resource) + end) + |> case do + [] -> + :ok + + resources -> + domain = Spark.Dsl.Verifier.get_persisted(dsl, :module) + + IO.warn(""" + Policies were defined on the domain `#{inspect(domain)}` but not on all resources. + + Domain policies are not applied to resources with no policies of their own. + + To address this, add the `Ash.Policy.Authorizer` authorizer to the + following resources. + + #{Enum.map_join(resources, "\n", &"* #{inspect(&1)}")} + + The following can be added to resources that have no policy rules + of their own like so: + + policies do + policy always() do + authorize_if always() + end + end + + All policies that apply to a request must pass, so the above policies + will not prevent the domain's policies from being applied. + """) + + :ok + end + end +end diff --git a/lib/ash/reactor/dsl/action_transformer.ex b/lib/ash/reactor/dsl/action_transformer.ex index 072a7250d..505ea104b 100644 --- a/lib/ash/reactor/dsl/action_transformer.ex +++ b/lib/ash/reactor/dsl/action_transformer.ex @@ -99,7 +99,7 @@ defmodule Ash.Reactor.Dsl.ActionTransformer do else {:error, DslError.exception( - module: Transformer.get_entities(dsl_state, :module), + module: Transformer.get_persisted(dsl_state, :module), path: [:reactor, entity.type, entity.name], message: "The #{entity.type} step `#{inspect(entity.name)}` has its domain set to `#{inspect(entity.domain)}` but it is not a valid `Ash.Domain`." diff --git a/test/support/policy_rbac/domain.ex b/test/support/policy_rbac/domain.ex index 687f48d33..95def1d65 100644 --- a/test/support/policy_rbac/domain.ex +++ b/test/support/policy_rbac/domain.ex @@ -1,6 +1,6 @@ defmodule Ash.Test.Support.PolicyRbac.Domain do @moduledoc false - use Ash.Domain + use Ash.Domain, authorizers: [Ash.Policy.Authorizer] resources do resource Ash.Test.Support.PolicyRbac.User From 6409b894305c428d74f5d82f1ee3b20cb46287c0 Mon Sep 17 00:00:00 2001 From: Zach Daniel Date: Mon, 13 Jan 2025 12:49:00 -0500 Subject: [PATCH 02/22] chore: remove authorizer from test --- test/support/policy_rbac/domain.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/support/policy_rbac/domain.ex b/test/support/policy_rbac/domain.ex index 95def1d65..687f48d33 100644 --- a/test/support/policy_rbac/domain.ex +++ b/test/support/policy_rbac/domain.ex @@ -1,6 +1,6 @@ defmodule Ash.Test.Support.PolicyRbac.Domain do @moduledoc false - use Ash.Domain, authorizers: [Ash.Policy.Authorizer] + use Ash.Domain resources do resource Ash.Test.Support.PolicyRbac.User From fdea4ae7d91c2efaeeb9e7a7616dbd263510c124 Mon Sep 17 00:00:00 2001 From: Chaz Watkins Date: Mon, 13 Jan 2025 17:03:32 -0600 Subject: [PATCH 03/22] improvement: Add autogenerate_enabled? to Ash.Type for Ecto compatability (#1715) Co-authored-by: Chaz Watkins --- lib/ash/type/type.ex | 9 +++++++++ lib/ash/type/uuid.ex | 3 ++- lib/ash/type/uuid_v7.ex | 3 ++- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/lib/ash/type/type.ex b/lib/ash/type/type.ex index 2d61e1b73..48cb26b9a 100644 --- a/lib/ash/type/type.ex +++ b/lib/ash/type/type.ex @@ -1638,6 +1638,15 @@ defmodule Ash.Type do @impl true def embed_as(_, _), do: :self + + if Keyword.get(unquote(opts), :autogenerate_enabled?) do + @impl true + def autogenerate(constraints) do + constraints + |> @parent.generator() + |> Enum.at(0) + end + end end @impl true diff --git a/lib/ash/type/uuid.ex b/lib/ash/type/uuid.ex index eee9fe717..2f23b8c81 100644 --- a/lib/ash/type/uuid.ex +++ b/lib/ash/type/uuid.ex @@ -5,7 +5,8 @@ defmodule Ash.Type.UUID do A builtin type that can be referenced via `:uuid` """ - use Ash.Type + use Ash.Type, + autogenerate_enabled?: true @impl true def storage_type(_), do: :uuid diff --git a/lib/ash/type/uuid_v7.ex b/lib/ash/type/uuid_v7.ex index d039a4d57..1ee83dbb7 100644 --- a/lib/ash/type/uuid_v7.ex +++ b/lib/ash/type/uuid_v7.ex @@ -5,7 +5,8 @@ defmodule Ash.Type.UUIDv7 do A builtin type that can be referenced via `:uuid_v7` """ - use Ash.Type + use Ash.Type, + autogenerate_enabled?: true @impl true def storage_type(_), do: :uuid From fa967ed9ee966c5f7acfa93b6c3ec8fb4076eca8 Mon Sep 17 00:00:00 2001 From: Adam Tharani <30530469+adamtharani@users.noreply.github.com> Date: Tue, 14 Jan 2025 12:41:18 -0500 Subject: [PATCH 04/22] =?UTF-8?q?fix:=20don=E2=80=99t=20require=20multiten?= =?UTF-8?q?ancy=20attribute=20in=20`get`=20(#1716)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Adam Tharani --- lib/ash/filter/filter.ex | 12 ++++++ test/filter/multitenancy_test.exs | 62 +++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 test/filter/multitenancy_test.exs diff --git a/lib/ash/filter/filter.ex b/lib/ash/filter/filter.ex index bf1a50f75..b1dc43aa4 100644 --- a/lib/ash/filter/filter.ex +++ b/lib/ash/filter/filter.ex @@ -471,6 +471,18 @@ defmodule Ash.Filter do end {fields, value} -> + multitenancy_attribute = Ash.Resource.Info.multitenancy_attribute(resource) + fields = Enum.reject(fields, fn key -> key == multitenancy_attribute end) + + {keyval?, value} = + case fields do + [field] when not keyval? -> + {true, [{field, value}]} + + _ -> + {keyval?, value} + end + if keyval? do with :error <- get_keys(value, fields, resource), :error <- get_identity_filter(resource, id) do diff --git a/test/filter/multitenancy_test.exs b/test/filter/multitenancy_test.exs new file mode 100644 index 000000000..7102beea7 --- /dev/null +++ b/test/filter/multitenancy_test.exs @@ -0,0 +1,62 @@ +defmodule Ash.Test.MultitenancyTest do + @moduledoc false + use ExUnit.Case, async: true + + alias Ash.Test.Domain, as: Domain + + defmodule MultiTenant do + use Ash.Resource, domain: Domain, data_layer: Ash.DataLayer.Ets + + multitenancy do + strategy :attribute + attribute :owner + end + + attributes do + attribute :id, :integer, primary_key?: true, allow_nil?: false, public?: true + attribute :owner, :integer, primary_key?: true, allow_nil?: false, public?: true + end + + actions do + default_accept :* + defaults [:read, :create] + end + end + + test "reading an object doesn't require multitenancy attribute in the primary key" do + MultiTenant + |> Ash.Changeset.for_create(:create, %{id: 1000, owner: 1}) + |> Ash.create!(tenant: 1) + + MultiTenant + |> Ash.get!(1000, tenant: 1) + end + + defmodule NonMultiTenant do + use Ash.Resource, domain: Domain, data_layer: Ash.DataLayer.Ets + + attributes do + attribute :id, :integer, primary_key?: true, allow_nil?: false, public?: true + attribute :owner, :integer, primary_key?: true, allow_nil?: false, public?: true + end + + actions do + default_accept :* + defaults [:read, :create] + end + end + + test "reading an object without multitenancy requires attribute in the primary key" do + NonMultiTenant + |> Ash.Changeset.for_create(:create, %{id: 1000, owner: 1}) + |> Ash.create!() + + ExUnit.Assertions.assert_raise(Ash.Error.Invalid, fn -> + NonMultiTenant + |> Ash.get!(1000) + end) + + NonMultiTenant + |> Ash.get!(%{id: 1000, owner: 1}) + end +end From 0b73b3ae3e309522004d941e200956cc3391dbb7 Mon Sep 17 00:00:00 2001 From: Zach Daniel Date: Tue, 14 Jan 2025 21:06:02 -0500 Subject: [PATCH 05/22] test: add tunez to test_subprojects --- .github/workflows/test-subprojects.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test-subprojects.yml b/.github/workflows/test-subprojects.yml index 0cbc41bb6..01027d3c7 100644 --- a/.github/workflows/test-subprojects.yml +++ b/.github/workflows/test-subprojects.yml @@ -28,6 +28,7 @@ jobs: { org: "ash-project", name: "ash_paper_trail" }, { org: "team-alembic", name: "ash_authentication" }, { org: "team-alembic", name: "ash_authentication_phoenix" }, + { org: "sevenseacat", name: "tunez" } ] otp: ["26.0.2"] elixir: ["1.16.0"] From a3c349165eb2601debfaad83ce55e6ad6067a787 Mon Sep 17 00:00:00 2001 From: Zach Daniel Date: Tue, 14 Jan 2025 21:09:59 -0500 Subject: [PATCH 06/22] test: test subprojects on newer versions --- .github/workflows/test-subprojects.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-subprojects.yml b/.github/workflows/test-subprojects.yml index 01027d3c7..3265e0606 100644 --- a/.github/workflows/test-subprojects.yml +++ b/.github/workflows/test-subprojects.yml @@ -30,8 +30,8 @@ jobs: { org: "team-alembic", name: "ash_authentication_phoenix" }, { org: "sevenseacat", name: "tunez" } ] - otp: ["26.0.2"] - elixir: ["1.16.0"] + otp: ["27.2"] + elixir: ["1.18.1"] services: pg: image: postgres:16 From 057b074c5ff61bd264c8ea30c21b471f5dd879ef Mon Sep 17 00:00:00 2001 From: Zach Daniel Date: Wed, 15 Jan 2025 16:18:51 -0500 Subject: [PATCH 07/22] fix: simplify and fix path generation for nested relationship path deps --- lib/ash/actions/read/calculations.ex | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/lib/ash/actions/read/calculations.ex b/lib/ash/actions/read/calculations.ex index ca841e5a6..a7274bffb 100644 --- a/lib/ash/actions/read/calculations.ex +++ b/lib/ash/actions/read/calculations.ex @@ -842,20 +842,16 @@ defmodule Ash.Actions.Read.Calculations do match?({:__calc_dep__, _}, calculation.name) end) |> Enum.flat_map(fn {_calc_name, calculation} -> - relationship = calculation.opts[:relationship] - query = calculation.opts[:query] - - query - |> get_all_rewrites(top_calculation, path) - |> Enum.map(fn {{path, data, calc_name, calc_load}, source} -> - {{path ++ [{:rel, relationship}], data, calc_name, calc_load}, source} - end) + get_all_rewrites( + calculation.opts[:query], + top_calculation, + path ++ [{:rel, calculation.opts[:relationship]}] + ) end) end # TODO: This currently must assume that all relationship loads are different if # authorize?: true, because the policies have not yet been applied. - # def split_and_load_calculations( domain, From fcba9fdede577b3c0e88ed6b69c9976d91d8ea2a Mon Sep 17 00:00:00 2001 From: Barnabas Jovanovics Date: Thu, 16 Jan 2025 16:23:02 +0100 Subject: [PATCH 08/22] feat: make atomics work even if expr err is not supported (#1718) --- lib/ash/changeset/changeset.ex | 42 +++++++++++++++++++++----------- lib/ash/expr/expr.ex | 8 +++++- lib/ash/filter/runtime.ex | 3 +++ lib/ash/query/operator/is_nil.ex | 2 +- 4 files changed, 39 insertions(+), 16 deletions(-) diff --git a/lib/ash/changeset/changeset.ex b/lib/ash/changeset/changeset.ex index f4e962f74..3f433b97c 100644 --- a/lib/ash/changeset/changeset.ex +++ b/lib/ash/changeset/changeset.ex @@ -867,17 +867,22 @@ defmodule Ash.Changeset do @doc false def run_atomic_validation(changeset, %{where: where} = validation, context) do - with {:atomic, condition} <- atomic_condition(where, changeset, context) do - case condition do - false -> - changeset + if Ash.DataLayer.data_layer_can?(changeset.resource, :expr_error) do + with {:atomic, condition} <- atomic_condition(where, changeset, context) do + case condition do + false -> + changeset - true -> - do_run_atomic_validation(changeset, validation, context) + true -> + do_run_atomic_validation(changeset, validation, context) - where_condition -> - do_run_atomic_validation(changeset, validation, context, where_condition) + where_condition -> + do_run_atomic_validation(changeset, validation, context, where_condition) + end end + else + {:not_atomic, + "data layer `#{Ash.DataLayer.data_layer(changeset.resource)}` does not support the expr_error"} end end @@ -1767,10 +1772,10 @@ defmodule Ash.Changeset do allow_nil? = attribute.allow_nil? and attribute.name not in changeset.action.require_attributes - value = - if allow_nil? || not Ash.Expr.can_return_nil?(value) do - value - else + if allow_nil? || not Ash.Expr.can_return_nil?(value) do + value + else + if Ash.DataLayer.data_layer_can?(changeset.resource, :expr_error) do expr( if is_nil(^value) do error( @@ -1785,9 +1790,18 @@ defmodule Ash.Changeset do ^value end ) + else + {:not_atomic, + "Failed to validate expression #{inspect(value)}: data layer `#{Ash.DataLayer.data_layer(changeset.resource)}` does not support the expr_error"} end + end + |> case do + {:not_atomic, error} -> + Ash.Changeset.add_error(changeset, error) - %{changeset | atomics: Keyword.put(changeset.atomics, key, value)} + value -> + %{changeset | atomics: Keyword.put(changeset.atomics, key, value)} + end end end) |> Ash.Changeset.hydrate_atomic_refs(actor, eager?: true) @@ -2715,7 +2729,7 @@ defmodule Ash.Changeset do if key in changeset.no_atomic_constraints do value = - if attribute.primary_key? do + if(attribute.primary_key?) do value else set_error_field(value, attribute.name) diff --git a/lib/ash/expr/expr.ex b/lib/ash/expr/expr.ex index 385caee5a..2223477e8 100644 --- a/lib/ash/expr/expr.ex +++ b/lib/ash/expr/expr.ex @@ -266,7 +266,13 @@ defmodule Ash.Expr do def can_return_nil?(%Ash.Query.Ref{attribute: %{allow_nil?: false}}), do: false - def can_return_nil?(_), do: true + def can_return_nil?(value) do + if Ash.Expr.expr?(value) do + true + else + false + end + end @doc "Whether or not a given template contains an actor reference" def template_references?(%{__struct__: Ash.Filter, expression: expression}, pred) do diff --git a/lib/ash/filter/runtime.ex b/lib/ash/filter/runtime.ex index 451c8546d..951e474c1 100644 --- a/lib/ash/filter/runtime.ex +++ b/lib/ash/filter/runtime.ex @@ -653,6 +653,9 @@ defmodule Ash.Filter.Runtime do {:op, {:ok, expr}} -> resolve_expr(expr, record, parent, resource, unknown_on_unknown_refs?) + {:op, {:known, value}} -> + {:ok, value} + {:error, error} -> {:error, error} diff --git a/lib/ash/query/operator/is_nil.ex b/lib/ash/query/operator/is_nil.ex index 7eb9d0c54..a2b65385e 100644 --- a/lib/ash/query/operator/is_nil.ex +++ b/lib/ash/query/operator/is_nil.ex @@ -15,7 +15,7 @@ defmodule Ash.Query.Operator.IsNil do def new(left, right) do if right == false and not Ash.Expr.can_return_nil?(left) do - {:known, false} + {:known, true} else super(left, right) end From 2102b050a66502bece65a760f9912b2ae13d6435 Mon Sep 17 00:00:00 2001 From: Steve Brambilla Date: Thu, 16 Jan 2025 16:27:17 -0500 Subject: [PATCH 09/22] fix: matching in managed_relationships handle_update (#1719) --- lib/ash/actions/managed_relationships.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/ash/actions/managed_relationships.ex b/lib/ash/actions/managed_relationships.ex index 2184eb11b..234655349 100644 --- a/lib/ash/actions/managed_relationships.ex +++ b/lib/ash/actions/managed_relationships.ex @@ -1367,7 +1367,7 @@ defmodule Ash.Actions.ManagedRelationships do {input, %{}} is_struct(input) -> - Map.from_struct(input) + {match, Map.from_struct(input)} true -> {match, input} From b917beaa378a3625ba73375fbe007dd24b7333d1 Mon Sep 17 00:00:00 2001 From: Zach Daniel Date: Thu, 16 Jan 2025 20:04:47 -0500 Subject: [PATCH 10/22] fix: handle related non-expr calculations referenced from expr calcs --- lib/ash/filter/runtime.ex | 42 ++++---- lib/ash/resource/calculation/expression.ex | 39 +++++-- .../resource/calculation/load_relationship.ex | 5 +- ...expr_calculation_with_related_dep_test.exs | 102 ++++++++++++++++++ 4 files changed, 161 insertions(+), 27 deletions(-) create mode 100644 test/calculations/expr_calculation_with_related_dep_test.exs diff --git a/lib/ash/filter/runtime.ex b/lib/ash/filter/runtime.ex index 951e474c1..89d14977c 100644 --- a/lib/ash/filter/runtime.ex +++ b/lib/ash/filter/runtime.ex @@ -892,30 +892,34 @@ defmodule Ash.Filter.Runtime do |> resolve_expr(record, parent, resource, unknown_on_unknown_refs?) end else - # We need to rewrite this - # As it stands now, it will evaluate the calculation - # once per expanded result. I'm not sure what that will - # look like though. + if unknown_on_unknown_refs? do + :unknown + else + # We need to rewrite this + # As it stands now, it will evaluate the calculation + # once per expanded result. I'm not sure what that will + # look like though. - if record do - case module.calculate([record], opts, context) do - [result] -> - {:ok, result} + if record do + case module.calculate([record], opts, context) do + [result] -> + {:ok, result} - {:ok, [result]} -> - {:ok, result} + {:ok, [result]} -> + {:ok, result} - :unknown when unknown_on_unknown_refs? -> - :unknown + :unknown when unknown_on_unknown_refs? -> + :unknown - _ -> - {:ok, nil} - end - else - if unknown_on_unknown_refs? do - :unknown + _ -> + {:ok, nil} + end else - {:ok, nil} + if unknown_on_unknown_refs? do + :unknown + else + {:ok, nil} + end end end end diff --git a/lib/ash/resource/calculation/expression.ex b/lib/ash/resource/calculation/expression.ex index 7dc080399..e4bdca2f5 100644 --- a/lib/ash/resource/calculation/expression.ex +++ b/lib/ash/resource/calculation/expression.ex @@ -113,15 +113,16 @@ defmodule Ash.Resource.Calculation.Expression do {:ok, expression} -> expression |> Ash.Filter.list_refs() - |> Enum.map(fn - %{attribute: %Ash.Query.Aggregate{} = agg} -> - agg + |> Enum.reduce([], fn + %{attribute: %Ash.Query.Aggregate{} = agg, relationship_path: relationship_path}, acc -> + add_at_path(acc, relationship_path, agg) - %{attribute: %Ash.Query.Calculation{} = calc} -> - calc + %{attribute: %Ash.Query.Calculation{} = calc, relationship_path: relationship_path}, + acc -> + add_at_path(acc, relationship_path, calc) - %{attribute: %{name: name}} -> - name + %{attribute: %{name: name}, relationship_path: relationship_path}, acc -> + add_at_path(acc, relationship_path, name) end) |> Enum.concat(Ash.Filter.used_aggregates(expression)) |> Enum.uniq() @@ -130,4 +131,28 @@ defmodule Ash.Resource.Calculation.Expression do [] end end + + defp add_at_path(acc, [], value) do + Enum.uniq([value | acc]) + end + + defp add_at_path(acc, [first | rest], value) do + Enum.reduce(acc, {acc, false}, fn + {key, current}, {acc, _found?} when key == first -> + {[{key, add_at_path(List.wrap(current), rest, value)} | acc], true} + + key, {acc, _found?} when key == first -> + {[{key, add_at_path([], rest, value)} | acc], true} + + v, {acc, found?} -> + {[v | acc], found?} + end) + |> case do + {acc, false} -> + [{first, add_at_path([], rest, value)} | acc] + + {acc, _} -> + acc + end + end end diff --git a/lib/ash/resource/calculation/load_relationship.ex b/lib/ash/resource/calculation/load_relationship.ex index 9515198a4..0610ab3ed 100644 --- a/lib/ash/resource/calculation/load_relationship.ex +++ b/lib/ash/resource/calculation/load_relationship.ex @@ -35,7 +35,10 @@ defmodule Ash.Resource.Calculation.LoadRelationship do end load_opts = - Ash.Context.to_opts(context, Keyword.put(opts[:opts] || [], :domain, opts[:domain])) + Ash.Context.to_opts( + context, + Keyword.put(opts[:opts] || [], :domain, opts[:domain]) + ) Ash.load(results, [{relationship.name, query}], load_opts) |> case do diff --git a/test/calculations/expr_calculation_with_related_dep_test.exs b/test/calculations/expr_calculation_with_related_dep_test.exs new file mode 100644 index 000000000..36b085380 --- /dev/null +++ b/test/calculations/expr_calculation_with_related_dep_test.exs @@ -0,0 +1,102 @@ +defmodule Ash.Test.ExprCalculationWithRelatedDepTest do + @moduledoc false + use ExUnit.Case, async: true + + defmodule Balance do + use Ash.Resource.Calculation + + @impl Ash.Resource.Calculation + def load(_, _, _), do: :balance_attr + + @impl Ash.Resource.Calculation + def calculate(accounts, _, context) do + opts = Ash.Context.to_opts(context) + + if opts[:actor] != %{a: :b} do + raise "actor not correct" + end + + if opts[:authorize?] do + raise "should not be authorizing" + end + + {:ok, Enum.map(accounts, fn account -> account.balance_attr end)} + end + end + + defmodule Account2 do + use Ash.Resource, + data_layer: Ash.DataLayer.Ets, + domain: Ash.Test.Domain + + ets do + private? true + end + + actions do + defaults([:read, create: :*]) + end + + attributes do + uuid_primary_key(:id) + + attribute(:type, :string, public?: true) + attribute(:balance_attr, :integer, public?: true) + + timestamps() + end + + calculations do + calculate :balance, :integer, Balance + end + + relationships do + belongs_to :account, Account do + public? true + allow_nil? false + end + end + end + + defmodule Account do + use Ash.Resource, + data_layer: Ash.DataLayer.Ets, + domain: Ash.Test.Domain + + ets do + private? true + end + + actions do + defaults([:read, create: :*]) + end + + attributes do + uuid_primary_key(:id) + + attribute(:type, :string, public?: true) + + timestamps() + end + + calculations do + calculate(:balance, :integer, expr(related_account.balance)) + end + + relationships do + has_one :related_account, Account2 do + public?(true) + destination_attribute(:account_id) + end + end + end + + test "can load non-expression calculations from expressions" do + account2 = + Ash.Seed.seed!(Account2, %{type: :test, balance_attr: 10}) + + account = Ash.Seed.seed!(Account, %{related_account: account2}) + + assert Ash.load!(account, :balance, authorize?: true, actor: %{a: :b}).balance == 10 + end +end From 94718b74461a2b907d8ff99d5a5926f982d02101 Mon Sep 17 00:00:00 2001 From: Zach Daniel Date: Thu, 16 Jan 2025 20:25:24 -0500 Subject: [PATCH 11/22] fix: use `Jason` always for compatibility --- lib/ash/helpers.ex | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/ash/helpers.ex b/lib/ash/helpers.ex index db7916071..238249913 100644 --- a/lib/ash/helpers.ex +++ b/lib/ash/helpers.ex @@ -649,10 +649,12 @@ defmodule Ash.Helpers do end def json_module do - if Code.ensure_loaded?(JSON) do - JSON - else - Jason - end + # there are libraries depending on our use of `Jason` here + # we are going to be stuck with `Jason` for a while + # if Code.ensure_loaded?(JSON) do + # JSON + # else + Jason + # end end end From 98341ad961e6f1e43f2220abfed8fc89ecae7347 Mon Sep 17 00:00:00 2001 From: Zach Daniel Date: Thu, 16 Jan 2025 20:31:14 -0500 Subject: [PATCH 12/22] chore: fix dialyzer error --- lib/ash/filter/runtime.ex | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/ash/filter/runtime.ex b/lib/ash/filter/runtime.ex index 89d14977c..a01ea19f3 100644 --- a/lib/ash/filter/runtime.ex +++ b/lib/ash/filter/runtime.ex @@ -908,9 +908,6 @@ defmodule Ash.Filter.Runtime do {:ok, [result]} -> {:ok, result} - :unknown when unknown_on_unknown_refs? -> - :unknown - _ -> {:ok, nil} end From d1662999632d117bcb0a394f8405ad2806013b95 Mon Sep 17 00:00:00 2001 From: Zach Daniel Date: Fri, 17 Jan 2025 23:20:07 -0500 Subject: [PATCH 13/22] fix: properly load doubly nested calculation's explicit dependencies fixes #1720 --- lib/ash/query/query.ex | 39 ++++++++++++------- ...expr_calculation_with_related_dep_test.exs | 39 ++++++++++++++++++- 2 files changed, 63 insertions(+), 15 deletions(-) diff --git a/lib/ash/query/query.ex b/lib/ash/query/query.ex index 5c5390b66..3b4838114 100644 --- a/lib/ash/query/query.ex +++ b/lib/ash/query/query.ex @@ -1488,23 +1488,34 @@ defmodule Ash.Query do module = calculation.module opts = calculation.opts - resource_calculation_load = - if resource_calculation do + if resource_calculation do + resource_calculation_load = List.wrap(resource_calculation.load) - else - [] - end - loads = - module.load( - query, - opts, - Map.put(calculation.context, :context, query.context) - ) - |> Ash.Actions.Helpers.validate_calculation_load!(module) - |> Enum.concat(resource_calculation_load) + loads = + module.load( + query, + opts, + Map.put(calculation.context, :context, query.context) + ) + |> Ash.Actions.Helpers.validate_calculation_load!(module) + |> Enum.concat(resource_calculation_load) - %{calculation | required_loads: loads} + %{calculation | required_loads: loads} + else + loads = + module.load( + query, + opts, + Map.put(calculation.context, :context, query.context) + ) + |> Ash.Actions.Helpers.validate_calculation_load!(module) + + %{ + calculation + | required_loads: Enum.concat(List.wrap(loads), List.wrap(calculation.required_loads)) + } + end end defp fetch_key(map, key) when is_map(map) do diff --git a/test/calculations/expr_calculation_with_related_dep_test.exs b/test/calculations/expr_calculation_with_related_dep_test.exs index 36b085380..8af2fa94f 100644 --- a/test/calculations/expr_calculation_with_related_dep_test.exs +++ b/test/calculations/expr_calculation_with_related_dep_test.exs @@ -51,7 +51,7 @@ defmodule Ash.Test.ExprCalculationWithRelatedDepTest do end relationships do - belongs_to :account, Account do + belongs_to :account, Ash.Test.ExprCalculationWithRelatedDepTest.Account do public? true allow_nil? false end @@ -81,6 +81,32 @@ defmodule Ash.Test.ExprCalculationWithRelatedDepTest do calculations do calculate(:balance, :integer, expr(related_account.balance)) + + calculate :related_account_account_count, :integer do + load related_account: :account + + calculation fn accounts, ctx -> + accounts + |> Enum.map(fn account -> + if match?(%Ash.NotLoaded{}, account.related_account.account) do + raise "nested dep not loaded" + end + + Enum.count(List.wrap(account.related_account.account)) + end) + end + end + + calculate :double_related_account_account_count, :integer do + load :related_account_account_count + + calculation fn accounts, ctx -> + accounts + |> Enum.map(fn account -> + account.related_account_account_count * 2 + end) + end + end end relationships do @@ -99,4 +125,15 @@ defmodule Ash.Test.ExprCalculationWithRelatedDepTest do assert Ash.load!(account, :balance, authorize?: true, actor: %{a: :b}).balance == 10 end + + test "can load nested calculation dependences from function calculations" do + account2 = + Ash.Seed.seed!(Account2, %{type: :test, balance_attr: 10}) + + account = Ash.Seed.seed!(Account, %{related_account: account2}) + + account = account |> Ash.load!(:double_related_account_account_count) + + assert account.double_related_account_account_count == 2 + end end From cee6d80c655f041f42ec47b187ba16708ad533c5 Mon Sep 17 00:00:00 2001 From: Rebecca Le <543859+sevenseacat@users.noreply.github.com> Date: Sun, 19 Jan 2025 01:01:47 +0800 Subject: [PATCH 14/22] improvement: Use clearer error message for match validation atomic errors (#1721) The previous error doesn't make it clear that its actually a validation issue, and which attribute is raising the error --- lib/ash/resource/validation/match.ex | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/ash/resource/validation/match.ex b/lib/ash/resource/validation/match.ex index c8a919da7..783d238dd 100644 --- a/lib/ash/resource/validation/match.ex +++ b/lib/ash/resource/validation/match.ex @@ -78,7 +78,8 @@ defmodule Ash.Resource.Validation.Match do validate(changeset, opts, context) not Ash.Changeset.changing_attribute?(changeset, opts[:attribute]) -> - {:not_atomic, "can't atomically match an attribute that is not changing"} + {:not_atomic, + "can't atomically run match validation on attribute `#{opts[:attribute]}` that is not changing"} atomic_expr_change?(changeset.atomics, opts[:attribute]) -> {:not_atomic, "can't match on an atomic expression"} From c78d94d8d85a8e9d0114fa88beb15ae4918421be Mon Sep 17 00:00:00 2001 From: Zach Daniel Date: Sat, 18 Jan 2025 19:04:43 -0500 Subject: [PATCH 15/22] docs: document `start_of_day/1-2` --- documentation/topics/reference/expressions.md | 1 + 1 file changed, 1 insertion(+) diff --git a/documentation/topics/reference/expressions.md b/documentation/topics/reference/expressions.md index 7aef9e085..1e718f100 100644 --- a/documentation/topics/reference/expressions.md +++ b/documentation/topics/reference/expressions.md @@ -93,6 +93,7 @@ For elixir-backed data layers, they will be a function or an MFA that will be ca - `from_now/2` | Same as `ago` but adds instead of subtracting - `datetime_add/3` | add an interval to a datetime, i.e `datetime_add(^datetime, 10, :hour)` - `date/3` | add an interval to a date, i.e `datetime_add(^date, 3, :day)` +- `start_of_day/1-2` | Converts a date or a datetime to the correspond start of its day (at 00:00 time). ## Primitives From ca0b9a41f1b18c267700be145688996f9e665824 Mon Sep 17 00:00:00 2001 From: Rebecca Le <543859+sevenseacat@users.noreply.github.com> Date: Sun, 19 Jan 2025 13:05:38 +0800 Subject: [PATCH 16/22] docs: Clarify the behaviour of `Ash.read_first` and `read_first!` by adding typespecs (#1722) I thought that `read_first!` would raise an error in the case of no results found - it won't --- lib/ash.ex | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/ash.ex b/lib/ash.ex index 0935da9ed..679a97cea 100644 --- a/lib/ash.ex +++ b/lib/ash.ex @@ -2104,8 +2104,10 @@ defmodule Ash do end @doc """ - Runs an ash query, returning the first result or raise an error. See `read_first/2` for more. + Runs an Ash query, returning the first result or nil, or raising an error. See `read_first/2` for more. """ + @spec read_first!(resource_or_query :: Ash.Query.t() | Ash.Resource.t(), opts :: Keyword.t()) :: + Ash.Resource.record() | nil @doc spark_opts: [{1, @read_one_opts_schema}] def read_first!(query, opts \\ []) do Ash.Helpers.expect_resource_or_query!(query) @@ -2125,6 +2127,8 @@ defmodule Ash do #{Spark.Options.docs(@read_one_opts_schema)} """ + @spec read_first(resource_or_query :: Ash.Query.t() | Ash.Resource.t(), opts :: Keyword.t()) :: + {:ok, Ash.Resource.record() | nil} | {:error, Ash.Error.t()} @doc spark_opts: [{1, @read_one_opts_schema}] def read_first(query, opts \\ []) do Ash.Helpers.expect_options!(opts) From 5673e8f30e02abbccaef57c056aaad6e91dd3d61 Mon Sep 17 00:00:00 2001 From: Zach Daniel Date: Sun, 19 Jan 2025 00:15:07 -0500 Subject: [PATCH 17/22] docs: make individual DSL options searchable --- README.md | 14 +- ....DataLayer.Ets.md => Ash.DataLayer.Ets.md} | 2 +- ...ayer.Mnesia.md => Ash.DataLayer.Mnesia.md} | 2 +- .../dsls/{DSL-Ash.Domain.md => Ash.Domain.md} | 8 +- ...ifier.PubSub.md => Ash.Notifier.PubSub.md} | 6 +- ...Authorizer.md => Ash.Policy.Authorizer.md} | 54 +++---- .../{DSL-Ash.Reactor.md => Ash.Reactor.md} | 134 +++++++++--------- .../{DSL-Ash.Resource.md => Ash.Resource.md} | 128 ++++++++--------- mix.exs | 27 ++-- mix.lock | 6 +- 10 files changed, 194 insertions(+), 187 deletions(-) rename documentation/dsls/{DSL-Ash.DataLayer.Ets.md => Ash.DataLayer.Ets.md} (97%) rename documentation/dsls/{DSL-Ash.DataLayer.Mnesia.md => Ash.DataLayer.Mnesia.md} (97%) rename documentation/dsls/{DSL-Ash.Domain.md => Ash.Domain.md} (98%) rename documentation/dsls/{DSL-Ash.Notifier.PubSub.md => Ash.Notifier.PubSub.md} (99%) rename documentation/dsls/{DSL-Ash.Policy.Authorizer.md => Ash.Policy.Authorizer.md} (96%) rename documentation/dsls/{DSL-Ash.Reactor.md => Ash.Reactor.md} (98%) rename documentation/dsls/{DSL-Ash.Resource.md => Ash.Resource.md} (98%) diff --git a/README.md b/README.md index c59356132..f3e2887d7 100644 --- a/README.md +++ b/README.md @@ -110,15 +110,15 @@ Welcome! Here you will find everything you need to know to get started with and ## Reference +- [Ash.Resource DSL](documentation/dsls/Ash.Resource.md) +- [Ash.Domain DSL](documentation/dsls/Ash.Domain.md) +- [Ash.Reactor DSL](documentation/dsls/Ash.Reactor.md) +- [Ash.Notifier.PubSub DSL](documentation/dsls/Ash.Notifier.PubSub.md) +- [Ash.Policy.Authorizer DSL](documentation/dsls/Ash.Policy.Authorizer.md) +- [Ash.DataLayer.Ets DSL](documentation/dsls/Ash.DataLayer.Ets.md) +- [Ash.DataLayer.Mnesia DSL](documentation/dsls/Ash.DataLayer.Mnesia.md) - [Glossary](documentation/topics/reference/glossary.md) - [Expressions](documentation/topics/reference/expressions.md) -- [Ash.Resource DSL](documentation/dsls/DSL-Ash.Resource.md) -- [Ash.Domain DSL](documentation/dsls/DSL-Ash.Domain.md) -- [Ash.Reactor DSL](documentation/dsls/DSL-Ash.Reactor.md) -- [Ash.Notifier.PubSub DSL](documentation/dsls/DSL-Ash.Notifier.PubSub.md) -- [Ash.Policy.Authorizer DSL](documentation/dsls/DSL-Ash.Policy.Authorizer.md) -- [Ash.DataLayer.Ets DSL](documentation/dsls/DSL-Ash.DataLayer.Ets.md) -- [Ash.DataLayer.Mnesia DSL](documentation/dsls/DSL-Ash.DataLayer.Mnesia.md) - For other reference documentation, see the sidebar & search bar ## Packages diff --git a/documentation/dsls/DSL-Ash.DataLayer.Ets.md b/documentation/dsls/Ash.DataLayer.Ets.md similarity index 97% rename from documentation/dsls/DSL-Ash.DataLayer.Ets.md rename to documentation/dsls/Ash.DataLayer.Ets.md index 7048a12d5..bb6a2f8ab 100644 --- a/documentation/dsls/DSL-Ash.DataLayer.Ets.md +++ b/documentation/dsls/Ash.DataLayer.Ets.md @@ -1,7 +1,7 @@ -# DSL: Ash.DataLayer.Ets +# Ash.DataLayer.Ets An ETS (Erlang Term Storage) backed Ash Datalayer, for testing and lightweight usage. diff --git a/documentation/dsls/DSL-Ash.DataLayer.Mnesia.md b/documentation/dsls/Ash.DataLayer.Mnesia.md similarity index 97% rename from documentation/dsls/DSL-Ash.DataLayer.Mnesia.md rename to documentation/dsls/Ash.DataLayer.Mnesia.md index 2b9cd6841..032eb19b3 100644 --- a/documentation/dsls/DSL-Ash.DataLayer.Mnesia.md +++ b/documentation/dsls/Ash.DataLayer.Mnesia.md @@ -1,7 +1,7 @@ -# DSL: Ash.DataLayer.Mnesia +# Ash.DataLayer.Mnesia An Mnesia backed Ash Datalayer. diff --git a/documentation/dsls/DSL-Ash.Domain.md b/documentation/dsls/Ash.Domain.md similarity index 98% rename from documentation/dsls/DSL-Ash.Domain.md rename to documentation/dsls/Ash.Domain.md index 27fe3df52..0c2f76213 100644 --- a/documentation/dsls/DSL-Ash.Domain.md +++ b/documentation/dsls/Ash.Domain.md @@ -1,7 +1,7 @@ -# DSL: Ash.Domain.Dsl +# Ash.Domain @@ -64,7 +64,7 @@ end -## resources.resource +### resources.resource ```elixir resource resource ``` @@ -92,7 +92,7 @@ resource Foo -## resources.resource.define +### resources.resource.define ```elixir define name ``` @@ -136,7 +136,7 @@ define :get_user_by_id, action: :get_by_id, args: [:id], get?: true Target: `Ash.Resource.Interface` -## resources.resource.define_calculation +### resources.resource.define_calculation ```elixir define_calculation name ``` diff --git a/documentation/dsls/DSL-Ash.Notifier.PubSub.md b/documentation/dsls/Ash.Notifier.PubSub.md similarity index 99% rename from documentation/dsls/DSL-Ash.Notifier.PubSub.md rename to documentation/dsls/Ash.Notifier.PubSub.md index 45d2ea480..1522b3dad 100644 --- a/documentation/dsls/DSL-Ash.Notifier.PubSub.md +++ b/documentation/dsls/Ash.Notifier.PubSub.md @@ -1,7 +1,7 @@ -# DSL: Ash.Notifier.PubSub +# Ash.Notifier.PubSub A builtin notifier to help you publish events over any kind of pub-sub tooling. @@ -159,7 +159,7 @@ end -## pub_sub.publish +### pub_sub.publish ```elixir publish action, topic ``` @@ -203,7 +203,7 @@ publish :assign, "assigned" Target: `Ash.Notifier.PubSub.Publication` -## pub_sub.publish_all +### pub_sub.publish_all ```elixir publish_all type, topic ``` diff --git a/documentation/dsls/DSL-Ash.Policy.Authorizer.md b/documentation/dsls/Ash.Policy.Authorizer.md similarity index 96% rename from documentation/dsls/DSL-Ash.Policy.Authorizer.md rename to documentation/dsls/Ash.Policy.Authorizer.md index 81fa76afc..2723806fd 100644 --- a/documentation/dsls/DSL-Ash.Policy.Authorizer.md +++ b/documentation/dsls/Ash.Policy.Authorizer.md @@ -1,7 +1,7 @@ -# DSL: Ash.Policy.Authorizer +# Ash.Policy.Authorizer An authorization extension for ash resources. @@ -95,7 +95,7 @@ end -## policies.policy +### policies.policy ```elixir policy condition \\ nil ``` @@ -151,7 +151,7 @@ See the [policies guide](/documentation/topics/security/policies.md) for more. | [`access_type`](#policies-policy-access_type){: #policies-policy-access_type } | `:strict \| :filter \| :runtime` | | Determines how the policy is applied. See the guide for more. | -## policies.policy.authorize_if +### policies.policy.authorize_if ```elixir authorize_if check ``` @@ -191,7 +191,7 @@ authorize_if actor_attribute_matches_record(:group, :group) Target: `Ash.Policy.Check` -## policies.policy.forbid_if +### policies.policy.forbid_if ```elixir forbid_if check ``` @@ -231,7 +231,7 @@ forbid_if actor_attribute_matches_record(:group, :blacklisted_groups) Target: `Ash.Policy.Check` -## policies.policy.authorize_unless +### policies.policy.authorize_unless ```elixir authorize_unless check ``` @@ -271,7 +271,7 @@ authorize_unless actor_attribute_matches_record(:group, :blacklisted_groups) Target: `Ash.Policy.Check` -## policies.policy.forbid_unless +### policies.policy.forbid_unless ```elixir forbid_unless check ``` @@ -318,7 +318,7 @@ Target: `Ash.Policy.Check` Target: `Ash.Policy.Policy` -## policies.policy_group +### policies.policy_group ```elixir policy_group condition ``` @@ -377,7 +377,7 @@ end -## policies.policy_group.policy +### policies.policy_group.policy ```elixir policy condition \\ nil ``` @@ -433,7 +433,7 @@ See the [policies guide](/documentation/topics/security/policies.md) for more. | [`access_type`](#policies-policy_group-policy-access_type){: #policies-policy_group-policy-access_type } | `:strict \| :filter \| :runtime` | | Determines how the policy is applied. See the guide for more. | -## policies.policy_group.policy.authorize_if +### policies.policy_group.policy.authorize_if ```elixir authorize_if check ``` @@ -473,7 +473,7 @@ authorize_if actor_attribute_matches_record(:group, :group) Target: `Ash.Policy.Check` -## policies.policy_group.policy.forbid_if +### policies.policy_group.policy.forbid_if ```elixir forbid_if check ``` @@ -513,7 +513,7 @@ forbid_if actor_attribute_matches_record(:group, :blacklisted_groups) Target: `Ash.Policy.Check` -## policies.policy_group.policy.authorize_unless +### policies.policy_group.policy.authorize_unless ```elixir authorize_unless check ``` @@ -553,7 +553,7 @@ authorize_unless actor_attribute_matches_record(:group, :blacklisted_groups) Target: `Ash.Policy.Check` -## policies.policy_group.policy.forbid_unless +### policies.policy_group.policy.forbid_unless ```elixir forbid_unless check ``` @@ -607,7 +607,7 @@ Target: `Ash.Policy.Policy` Target: `Ash.Policy.PolicyGroup` -## policies.bypass +### policies.bypass ```elixir bypass condition \\ nil ``` @@ -637,7 +637,7 @@ A policy that, if passed, will skip all following policies. If failed, authoriza | [`access_type`](#policies-bypass-access_type){: #policies-bypass-access_type } | `:strict \| :filter \| :runtime` | | Determines how the policy is applied. See the guide for more. | -## policies.bypass.authorize_if +### policies.bypass.authorize_if ```elixir authorize_if check ``` @@ -677,7 +677,7 @@ authorize_if actor_attribute_matches_record(:group, :group) Target: `Ash.Policy.Check` -## policies.bypass.forbid_if +### policies.bypass.forbid_if ```elixir forbid_if check ``` @@ -717,7 +717,7 @@ forbid_if actor_attribute_matches_record(:group, :blacklisted_groups) Target: `Ash.Policy.Check` -## policies.bypass.authorize_unless +### policies.bypass.authorize_unless ```elixir authorize_unless check ``` @@ -757,7 +757,7 @@ authorize_unless actor_attribute_matches_record(:group, :blacklisted_groups) Target: `Ash.Policy.Check` -## policies.bypass.forbid_unless +### policies.bypass.forbid_unless ```elixir forbid_unless check ``` @@ -887,7 +887,7 @@ end -## field_policies.field_policy_bypass +### field_policies.field_policy_bypass ```elixir field_policy_bypass fields, condition \\ nil ``` @@ -917,7 +917,7 @@ A field policy that, if passed, will skip all following field policies for that | [`description`](#field_policies-field_policy_bypass-description){: #field_policies-field_policy_bypass-description } | `String.t` | | A description for the policy, used when explaining authorization results | -## field_policies.field_policy_bypass.authorize_if +### field_policies.field_policy_bypass.authorize_if ```elixir authorize_if check ``` @@ -957,7 +957,7 @@ authorize_if actor_attribute_matches_record(:group, :group) Target: `Ash.Policy.Check` -## field_policies.field_policy_bypass.forbid_if +### field_policies.field_policy_bypass.forbid_if ```elixir forbid_if check ``` @@ -997,7 +997,7 @@ forbid_if actor_attribute_matches_record(:group, :blacklisted_groups) Target: `Ash.Policy.Check` -## field_policies.field_policy_bypass.authorize_unless +### field_policies.field_policy_bypass.authorize_unless ```elixir authorize_unless check ``` @@ -1037,7 +1037,7 @@ authorize_unless actor_attribute_matches_record(:group, :blacklisted_groups) Target: `Ash.Policy.Check` -## field_policies.field_policy_bypass.forbid_unless +### field_policies.field_policy_bypass.forbid_unless ```elixir forbid_unless check ``` @@ -1084,7 +1084,7 @@ Target: `Ash.Policy.Check` Target: `Ash.Policy.FieldPolicy` -## field_policies.field_policy +### field_policies.field_policy ```elixir field_policy fields, condition \\ nil ``` @@ -1116,7 +1116,7 @@ for more. | [`description`](#field_policies-field_policy-description){: #field_policies-field_policy-description } | `String.t` | | A description for the policy, used when explaining authorization results | -## field_policies.field_policy.authorize_if +### field_policies.field_policy.authorize_if ```elixir authorize_if check ``` @@ -1156,7 +1156,7 @@ authorize_if actor_attribute_matches_record(:group, :group) Target: `Ash.Policy.Check` -## field_policies.field_policy.forbid_if +### field_policies.field_policy.forbid_if ```elixir forbid_if check ``` @@ -1196,7 +1196,7 @@ forbid_if actor_attribute_matches_record(:group, :blacklisted_groups) Target: `Ash.Policy.Check` -## field_policies.field_policy.authorize_unless +### field_policies.field_policy.authorize_unless ```elixir authorize_unless check ``` @@ -1236,7 +1236,7 @@ authorize_unless actor_attribute_matches_record(:group, :blacklisted_groups) Target: `Ash.Policy.Check` -## field_policies.field_policy.forbid_unless +### field_policies.field_policy.forbid_unless ```elixir forbid_unless check ``` diff --git a/documentation/dsls/DSL-Ash.Reactor.md b/documentation/dsls/Ash.Reactor.md similarity index 98% rename from documentation/dsls/DSL-Ash.Reactor.md rename to documentation/dsls/Ash.Reactor.md index a973b97bc..30cbd5e98 100644 --- a/documentation/dsls/DSL-Ash.Reactor.md +++ b/documentation/dsls/Ash.Reactor.md @@ -1,7 +1,7 @@ -# DSL: Ash.Reactor +# Ash.Reactor `Ash.Reactor` is a [`Reactor`](https://hex.pm/packages/reactor) extension which provides steps for working with Ash resources and actions. @@ -28,7 +28,7 @@ Ash-related configuration for the `Ash.Reactor` extension -## reactor.action +### reactor.action ```elixir action name, resource, action \\ nil ``` @@ -75,7 +75,7 @@ Declares a step that will call a generic action on a resource. | [`undo`](#reactor-action-undo){: #reactor-action-undo } | `:always \| :never \| :outside_transaction` | `:never` | How to handle undoing this action | -## reactor.action.actor +### reactor.action.actor ```elixir actor source ``` @@ -106,7 +106,7 @@ Specifies the action actor Target: `Ash.Reactor.Dsl.Actor` -## reactor.action.context +### reactor.action.context ```elixir context context ``` @@ -137,7 +137,7 @@ A map to be merged into the action's context Target: `Ash.Reactor.Dsl.Context` -## reactor.action.inputs +### reactor.action.inputs ```elixir inputs template ``` @@ -183,7 +183,7 @@ inputs(author: result(:get_user)) Target: `Ash.Reactor.Dsl.Inputs` -## reactor.action.tenant +### reactor.action.tenant ```elixir tenant source ``` @@ -214,7 +214,7 @@ Specifies the action tenant Target: `Ash.Reactor.Dsl.Tenant` -## reactor.action.wait_for +### reactor.action.wait_for ```elixir wait_for names ``` @@ -262,7 +262,7 @@ Target: `Ash.Reactor.Dsl.Action` -## reactor.ash_step +### reactor.ash_step ```elixir ash_step name, impl \\ nil ``` @@ -320,7 +320,7 @@ end | [`transform`](#reactor-ash_step-transform){: #reactor-ash_step-transform } | `(any -> any) \| module \| nil` | | An optional transformation function which can be used to modify the entire argument map before it is passed to the step. | -## reactor.ash_step.argument +### reactor.ash_step.argument ```elixir argument name, source \\ nil ``` @@ -392,7 +392,7 @@ argument :three, value(3) Target: `Reactor.Dsl.Argument` -## reactor.ash_step.wait_for +### reactor.ash_step.wait_for ```elixir wait_for names ``` @@ -440,7 +440,7 @@ Target: `Ash.Reactor.Dsl.AshStep` -## reactor.bulk_create +### reactor.bulk_create ```elixir bulk_create name, resource, action \\ nil ``` @@ -530,7 +530,7 @@ end | [`undo`](#reactor-bulk_create-undo){: #reactor-bulk_create-undo } | `:always \| :never \| :outside_transaction` | `:never` | How to handle undoing this action | -## reactor.bulk_create.context +### reactor.bulk_create.context ```elixir context context ``` @@ -561,7 +561,7 @@ A map to be merged into the action's context Target: `Ash.Reactor.Dsl.Context` -## reactor.bulk_create.actor +### reactor.bulk_create.actor ```elixir actor source ``` @@ -592,7 +592,7 @@ Specifies the action actor Target: `Ash.Reactor.Dsl.Actor` -## reactor.bulk_create.load +### reactor.bulk_create.load ```elixir load source ``` @@ -623,7 +623,7 @@ Allows the addition of an Ash load statement to the action Target: `Ash.Reactor.Dsl.ActionLoad` -## reactor.bulk_create.tenant +### reactor.bulk_create.tenant ```elixir tenant source ``` @@ -654,7 +654,7 @@ Specifies the action tenant Target: `Ash.Reactor.Dsl.Tenant` -## reactor.bulk_create.wait_for +### reactor.bulk_create.wait_for ```elixir wait_for names ``` @@ -702,7 +702,7 @@ Target: `Ash.Reactor.Dsl.BulkCreate` -## reactor.bulk_update +### reactor.bulk_update ```elixir bulk_update name, resource, action \\ nil ``` @@ -798,7 +798,7 @@ end | [`undo`](#reactor-bulk_update-undo){: #reactor-bulk_update-undo } | `:always \| :never \| :outside_transaction` | `:never` | How to handle undoing this action | -## reactor.bulk_update.actor +### reactor.bulk_update.actor ```elixir actor source ``` @@ -829,7 +829,7 @@ Specifies the action actor Target: `Ash.Reactor.Dsl.Actor` -## reactor.bulk_update.context +### reactor.bulk_update.context ```elixir context context ``` @@ -860,7 +860,7 @@ A map to be merged into the action's context Target: `Ash.Reactor.Dsl.Context` -## reactor.bulk_update.inputs +### reactor.bulk_update.inputs ```elixir inputs template ``` @@ -906,7 +906,7 @@ inputs(author: result(:get_user)) Target: `Ash.Reactor.Dsl.Inputs` -## reactor.bulk_update.tenant +### reactor.bulk_update.tenant ```elixir tenant source ``` @@ -937,7 +937,7 @@ Specifies the action tenant Target: `Ash.Reactor.Dsl.Tenant` -## reactor.bulk_update.wait_for +### reactor.bulk_update.wait_for ```elixir wait_for names ``` @@ -985,7 +985,7 @@ Target: `Ash.Reactor.Dsl.BulkUpdate` -## reactor.change +### reactor.change ```elixir change name, change ``` @@ -1017,7 +1017,7 @@ Declares a step that will modify a changeset. | [`fail_if_invalid?`](#reactor-change-fail_if_invalid?){: #reactor-change-fail_if_invalid? } | `boolean` | `false` | Fail if the result of the change is an invalid changeset | -## reactor.change.argument +### reactor.change.argument ```elixir argument name, source \\ nil ``` @@ -1089,7 +1089,7 @@ argument :three, value(3) Target: `Reactor.Dsl.Argument` -## reactor.change.wait_for +### reactor.change.wait_for ```elixir wait_for names ``` @@ -1137,7 +1137,7 @@ Target: `Ash.Reactor.Dsl.Change` -## reactor.create +### reactor.create ```elixir create name, resource, action \\ nil ``` @@ -1201,7 +1201,7 @@ end | [`undo`](#reactor-create-undo){: #reactor-create-undo } | `:always \| :never \| :outside_transaction` | `:never` | How to handle undoing this action | -## reactor.create.actor +### reactor.create.actor ```elixir actor source ``` @@ -1232,7 +1232,7 @@ Specifies the action actor Target: `Ash.Reactor.Dsl.Actor` -## reactor.create.context +### reactor.create.context ```elixir context context ``` @@ -1263,7 +1263,7 @@ A map to be merged into the action's context Target: `Ash.Reactor.Dsl.Context` -## reactor.create.inputs +### reactor.create.inputs ```elixir inputs template ``` @@ -1309,7 +1309,7 @@ inputs(author: result(:get_user)) Target: `Ash.Reactor.Dsl.Inputs` -## reactor.create.load +### reactor.create.load ```elixir load source ``` @@ -1340,7 +1340,7 @@ Allows the addition of an Ash load statement to the action Target: `Ash.Reactor.Dsl.ActionLoad` -## reactor.create.tenant +### reactor.create.tenant ```elixir tenant source ``` @@ -1371,7 +1371,7 @@ Specifies the action tenant Target: `Ash.Reactor.Dsl.Tenant` -## reactor.create.wait_for +### reactor.create.wait_for ```elixir wait_for names ``` @@ -1419,7 +1419,7 @@ Target: `Ash.Reactor.Dsl.Create` -## reactor.destroy +### reactor.destroy ```elixir destroy name, resource, action \\ nil ``` @@ -1479,7 +1479,7 @@ end | [`undo`](#reactor-destroy-undo){: #reactor-destroy-undo } | `:always \| :never \| :outside_transaction` | `:never` | How to handle undoing this action | -## reactor.destroy.actor +### reactor.destroy.actor ```elixir actor source ``` @@ -1510,7 +1510,7 @@ Specifies the action actor Target: `Ash.Reactor.Dsl.Actor` -## reactor.destroy.context +### reactor.destroy.context ```elixir context context ``` @@ -1541,7 +1541,7 @@ A map to be merged into the action's context Target: `Ash.Reactor.Dsl.Context` -## reactor.destroy.inputs +### reactor.destroy.inputs ```elixir inputs template ``` @@ -1587,7 +1587,7 @@ inputs(author: result(:get_user)) Target: `Ash.Reactor.Dsl.Inputs` -## reactor.destroy.load +### reactor.destroy.load ```elixir load source ``` @@ -1618,7 +1618,7 @@ Allows the addition of an Ash load statement to the action Target: `Ash.Reactor.Dsl.ActionLoad` -## reactor.destroy.tenant +### reactor.destroy.tenant ```elixir tenant source ``` @@ -1649,7 +1649,7 @@ Specifies the action tenant Target: `Ash.Reactor.Dsl.Tenant` -## reactor.destroy.wait_for +### reactor.destroy.wait_for ```elixir wait_for names ``` @@ -1697,7 +1697,7 @@ Target: `Ash.Reactor.Dsl.Destroy` -## reactor.load +### reactor.load ```elixir load name, records, load ``` @@ -1735,7 +1735,7 @@ Declares a step that will load additional data on a resource. | [`strict?`](#reactor-load-strict?){: #reactor-load-strict? } | `boolean` | | If set to true, only specified attributes will be loaded when passing a list of fields to fetch on a relationship, which allows for more optimized data-fetching. | -## reactor.load.actor +### reactor.load.actor ```elixir actor source ``` @@ -1766,7 +1766,7 @@ Specifies the action actor Target: `Ash.Reactor.Dsl.Actor` -## reactor.load.context +### reactor.load.context ```elixir context context ``` @@ -1797,7 +1797,7 @@ A map to be merged into the action's context Target: `Ash.Reactor.Dsl.Context` -## reactor.load.tenant +### reactor.load.tenant ```elixir tenant source ``` @@ -1828,7 +1828,7 @@ Specifies the action tenant Target: `Ash.Reactor.Dsl.Tenant` -## reactor.load.wait_for +### reactor.load.wait_for ```elixir wait_for names ``` @@ -1876,7 +1876,7 @@ Target: `Ash.Reactor.Dsl.Load` -## reactor.read_one +### reactor.read_one ```elixir read_one name, resource, action \\ nil ``` @@ -1921,7 +1921,7 @@ end | [`description`](#reactor-read_one-description){: #reactor-read_one-description } | `String.t` | | A description for the step | -## reactor.read_one.actor +### reactor.read_one.actor ```elixir actor source ``` @@ -1952,7 +1952,7 @@ Specifies the action actor Target: `Ash.Reactor.Dsl.Actor` -## reactor.read_one.context +### reactor.read_one.context ```elixir context context ``` @@ -1983,7 +1983,7 @@ A map to be merged into the action's context Target: `Ash.Reactor.Dsl.Context` -## reactor.read_one.inputs +### reactor.read_one.inputs ```elixir inputs template ``` @@ -2029,7 +2029,7 @@ inputs(author: result(:get_user)) Target: `Ash.Reactor.Dsl.Inputs` -## reactor.read_one.load +### reactor.read_one.load ```elixir load source ``` @@ -2060,7 +2060,7 @@ Allows the addition of an Ash load statement to the action Target: `Ash.Reactor.Dsl.ActionLoad` -## reactor.read_one.tenant +### reactor.read_one.tenant ```elixir tenant source ``` @@ -2091,7 +2091,7 @@ Specifies the action tenant Target: `Ash.Reactor.Dsl.Tenant` -## reactor.read_one.wait_for +### reactor.read_one.wait_for ```elixir wait_for names ``` @@ -2139,7 +2139,7 @@ Target: `Ash.Reactor.Dsl.ReadOne` -## reactor.read +### reactor.read ```elixir read name, resource, action \\ nil ``` @@ -2188,7 +2188,7 @@ end | [`description`](#reactor-read-description){: #reactor-read-description } | `String.t` | | A description for the step | -## reactor.read.actor +### reactor.read.actor ```elixir actor source ``` @@ -2219,7 +2219,7 @@ Specifies the action actor Target: `Ash.Reactor.Dsl.Actor` -## reactor.read.context +### reactor.read.context ```elixir context context ``` @@ -2250,7 +2250,7 @@ A map to be merged into the action's context Target: `Ash.Reactor.Dsl.Context` -## reactor.read.inputs +### reactor.read.inputs ```elixir inputs template ``` @@ -2296,7 +2296,7 @@ inputs(author: result(:get_user)) Target: `Ash.Reactor.Dsl.Inputs` -## reactor.read.load +### reactor.read.load ```elixir load source ``` @@ -2327,7 +2327,7 @@ Allows the addition of an Ash load statement to the action Target: `Ash.Reactor.Dsl.ActionLoad` -## reactor.read.tenant +### reactor.read.tenant ```elixir tenant source ``` @@ -2358,7 +2358,7 @@ Specifies the action tenant Target: `Ash.Reactor.Dsl.Tenant` -## reactor.read.wait_for +### reactor.read.wait_for ```elixir wait_for names ``` @@ -2406,7 +2406,7 @@ Target: `Ash.Reactor.Dsl.Read` -## reactor.transaction +### reactor.transaction ```elixir transaction name, resources ``` @@ -2434,7 +2434,7 @@ Creates a group of steps which will be executed inside a data layer transaction. | [`timeout`](#reactor-transaction-timeout){: #reactor-transaction-timeout } | `pos_integer \| :infinity` | `15000` | How long to allow the transaction to run before timing out. | -## reactor.transaction.wait_for +### reactor.transaction.wait_for ```elixir wait_for names ``` @@ -2482,7 +2482,7 @@ Target: `Ash.Reactor.Dsl.Transaction` -## reactor.update +### reactor.update ```elixir update name, resource, action \\ nil ``` @@ -2544,7 +2544,7 @@ end | [`undo`](#reactor-update-undo){: #reactor-update-undo } | `:always \| :never \| :outside_transaction` | `:never` | How to handle undoing this action | -## reactor.update.actor +### reactor.update.actor ```elixir actor source ``` @@ -2575,7 +2575,7 @@ Specifies the action actor Target: `Ash.Reactor.Dsl.Actor` -## reactor.update.context +### reactor.update.context ```elixir context context ``` @@ -2606,7 +2606,7 @@ A map to be merged into the action's context Target: `Ash.Reactor.Dsl.Context` -## reactor.update.inputs +### reactor.update.inputs ```elixir inputs template ``` @@ -2652,7 +2652,7 @@ inputs(author: result(:get_user)) Target: `Ash.Reactor.Dsl.Inputs` -## reactor.update.load +### reactor.update.load ```elixir load source ``` @@ -2683,7 +2683,7 @@ Allows the addition of an Ash load statement to the action Target: `Ash.Reactor.Dsl.ActionLoad` -## reactor.update.tenant +### reactor.update.tenant ```elixir tenant source ``` @@ -2714,7 +2714,7 @@ Specifies the action tenant Target: `Ash.Reactor.Dsl.Tenant` -## reactor.update.wait_for +### reactor.update.wait_for ```elixir wait_for names ``` diff --git a/documentation/dsls/DSL-Ash.Resource.md b/documentation/dsls/Ash.Resource.md similarity index 98% rename from documentation/dsls/DSL-Ash.Resource.md rename to documentation/dsls/Ash.Resource.md index 5c33ec055..142b3fcc6 100644 --- a/documentation/dsls/DSL-Ash.Resource.md +++ b/documentation/dsls/Ash.Resource.md @@ -1,7 +1,7 @@ -# DSL: Ash.Resource.Dsl +# Ash.Resource @@ -54,7 +54,7 @@ end -## attributes.attribute +### attributes.attribute ```elixir attribute name, type ``` @@ -110,7 +110,7 @@ end Target: `Ash.Resource.Attribute` -## attributes.create_timestamp +### attributes.create_timestamp ```elixir create_timestamp name ``` @@ -154,7 +154,7 @@ create_timestamp :inserted_at Target: `Ash.Resource.Attribute` -## attributes.update_timestamp +### attributes.update_timestamp ```elixir update_timestamp name ``` @@ -199,7 +199,7 @@ update_timestamp :updated_at Target: `Ash.Resource.Attribute` -## attributes.integer_primary_key +### attributes.integer_primary_key ```elixir integer_primary_key name ``` @@ -245,7 +245,7 @@ integer_primary_key :id Target: `Ash.Resource.Attribute` -## attributes.uuid_primary_key +### attributes.uuid_primary_key ```elixir uuid_primary_key name ``` @@ -289,7 +289,7 @@ uuid_primary_key :id Target: `Ash.Resource.Attribute` -## attributes.uuid_v7_primary_key +### attributes.uuid_v7_primary_key ```elixir uuid_v7_primary_key name ``` @@ -399,7 +399,7 @@ end -## relationships.has_one +### relationships.has_one ```elixir has_one name, destination ``` @@ -461,7 +461,7 @@ end | [`allow_forbidden_field?`](#relationships-has_one-allow_forbidden_field?){: #relationships-has_one-allow_forbidden_field? } | `boolean` | `false` | If set to `true`, the relationship will be set to `%Ash.ForbiddenField{}` if its query produces a forbidden error. | -## relationships.has_one.filter +### relationships.has_one.filter ```elixir filter filter ``` @@ -502,7 +502,7 @@ Target: `Ash.Resource.Dsl.Filter` Target: `Ash.Resource.Relationships.HasOne` -## relationships.has_many +### relationships.has_many ```elixir has_many name, destination ``` @@ -560,7 +560,7 @@ end | [`allow_forbidden_field?`](#relationships-has_many-allow_forbidden_field?){: #relationships-has_many-allow_forbidden_field? } | `boolean` | `false` | If set to `true`, the relationship will be set to `%Ash.ForbiddenField{}` if its query produces a forbidden error. | -## relationships.has_many.filter +### relationships.has_many.filter ```elixir filter filter ``` @@ -601,7 +601,7 @@ Target: `Ash.Resource.Dsl.Filter` Target: `Ash.Resource.Relationships.HasMany` -## relationships.many_to_many +### relationships.many_to_many ```elixir many_to_many name, destination ``` @@ -670,7 +670,7 @@ belongs_to :word, Word, primary_key?: true, allow_nil?: false | [`allow_forbidden_field?`](#relationships-many_to_many-allow_forbidden_field?){: #relationships-many_to_many-allow_forbidden_field? } | `boolean` | `false` | If set to `true`, the relationship will be set to `%Ash.ForbiddenField{}` if its query produces a forbidden error. | -## relationships.many_to_many.filter +### relationships.many_to_many.filter ```elixir filter filter ``` @@ -711,7 +711,7 @@ Target: `Ash.Resource.Dsl.Filter` Target: `Ash.Resource.Relationships.ManyToMany` -## relationships.belongs_to +### relationships.belongs_to ```elixir belongs_to name, destination ``` @@ -774,7 +774,7 @@ end | [`allow_forbidden_field?`](#relationships-belongs_to-allow_forbidden_field?){: #relationships-belongs_to-allow_forbidden_field? } | `boolean` | `false` | If set to `true`, the relationship will be set to `%Ash.ForbiddenField{}` if its query produces a forbidden error. | -## relationships.belongs_to.filter +### relationships.belongs_to.filter ```elixir filter filter ``` @@ -898,7 +898,7 @@ end -## actions.action +### actions.action ```elixir action name, returns \\ nil ``` @@ -948,7 +948,7 @@ end | [`skip_unknown_inputs`](#actions-action-skip_unknown_inputs){: #actions-action-skip_unknown_inputs } | `atom \| String.t \| list(atom \| String.t)` | `[]` | A list of unknown fields to skip, or `:*` to skip all unknown fields. | -## actions.action.argument +### actions.action.argument ```elixir argument name, type ``` @@ -998,7 +998,7 @@ Target: `Ash.Resource.Actions.Argument` Target: `Ash.Resource.Actions.Action` -## actions.create +### actions.create ```elixir create name ``` @@ -1055,7 +1055,7 @@ end | [`manual?`](#actions-create-manual?){: #actions-create-manual? } | `boolean` | | Instructs Ash to *skip* the actual update/create/destroy step at the data layer. See the [manual actions guide](/documentation/topics/manual-actions.md) for more. | -## actions.create.change +### actions.create.change ```elixir change change ``` @@ -1101,7 +1101,7 @@ change {MyCustomChange, :foo} Target: `Ash.Resource.Change` -## actions.create.validate +### actions.create.validate ```elixir validate validation ``` @@ -1145,7 +1145,7 @@ validate changing(:email) Target: `Ash.Resource.Validation` -## actions.create.argument +### actions.create.argument ```elixir argument name, type ``` @@ -1188,7 +1188,7 @@ argument :password_confirmation, :string Target: `Ash.Resource.Actions.Argument` -## actions.create.metadata +### actions.create.metadata ```elixir metadata name, type ``` @@ -1243,7 +1243,7 @@ Target: `Ash.Resource.Actions.Metadata` Target: `Ash.Resource.Actions.Create` -## actions.read +### actions.read ```elixir read name ``` @@ -1292,7 +1292,7 @@ end | [`skip_unknown_inputs`](#actions-read-skip_unknown_inputs){: #actions-read-skip_unknown_inputs } | `atom \| String.t \| list(atom \| String.t)` | `[]` | A list of unknown fields to skip, or `:*` to skip all unknown fields. | -## actions.read.argument +### actions.read.argument ```elixir argument name, type ``` @@ -1335,7 +1335,7 @@ argument :password_confirmation, :string Target: `Ash.Resource.Actions.Argument` -## actions.read.prepare +### actions.read.prepare ```elixir prepare preparation ``` @@ -1369,7 +1369,7 @@ prepare build(sort: [:foo, :bar]) Target: `Ash.Resource.Preparation` -## actions.read.pagination +### actions.read.pagination Adds pagination options to a resource @@ -1399,7 +1399,7 @@ Adds pagination options to a resource Target: `Ash.Resource.Actions.Read.Pagination` -## actions.read.metadata +### actions.read.metadata ```elixir metadata name, type ``` @@ -1447,7 +1447,7 @@ metadata :operation_id, :string, allow_nil?: false Target: `Ash.Resource.Actions.Metadata` -## actions.read.filter +### actions.read.filter ```elixir filter filter ``` @@ -1488,7 +1488,7 @@ Target: `Ash.Resource.Dsl.Filter` Target: `Ash.Resource.Actions.Read` -## actions.update +### actions.update ```elixir update name ``` @@ -1540,7 +1540,7 @@ update :flag_for_review, primary?: true | [`manual?`](#actions-update-manual?){: #actions-update-manual? } | `boolean` | | Instructs Ash to *skip* the actual update/create/destroy step at the data layer. See the [manual actions guide](/documentation/topics/manual-actions.md) for more. | -## actions.update.change +### actions.update.change ```elixir change change ``` @@ -1586,7 +1586,7 @@ change {MyCustomChange, :foo} Target: `Ash.Resource.Change` -## actions.update.validate +### actions.update.validate ```elixir validate validation ``` @@ -1630,7 +1630,7 @@ validate changing(:email) Target: `Ash.Resource.Validation` -## actions.update.metadata +### actions.update.metadata ```elixir metadata name, type ``` @@ -1678,7 +1678,7 @@ metadata :operation_id, :string, allow_nil?: false Target: `Ash.Resource.Actions.Metadata` -## actions.update.argument +### actions.update.argument ```elixir argument name, type ``` @@ -1728,7 +1728,7 @@ Target: `Ash.Resource.Actions.Argument` Target: `Ash.Resource.Actions.Update` -## actions.destroy +### actions.destroy ```elixir destroy name ``` @@ -1784,7 +1784,7 @@ end | [`manual?`](#actions-destroy-manual?){: #actions-destroy-manual? } | `boolean` | | Instructs Ash to *skip* the actual update/create/destroy step at the data layer. See the [manual actions guide](/documentation/topics/manual-actions.md) for more. | -## actions.destroy.change +### actions.destroy.change ```elixir change change ``` @@ -1830,7 +1830,7 @@ change {MyCustomChange, :foo} Target: `Ash.Resource.Change` -## actions.destroy.validate +### actions.destroy.validate ```elixir validate validation ``` @@ -1874,7 +1874,7 @@ validate changing(:email) Target: `Ash.Resource.Validation` -## actions.destroy.metadata +### actions.destroy.metadata ```elixir metadata name, type ``` @@ -1922,7 +1922,7 @@ metadata :operation_id, :string, allow_nil?: false Target: `Ash.Resource.Actions.Metadata` -## actions.destroy.argument +### actions.destroy.argument ```elixir argument name, type ``` @@ -2005,7 +2005,7 @@ end -## code_interface.define +### code_interface.define ```elixir define name ``` @@ -2049,7 +2049,7 @@ define :get_user_by_id, action: :get_by_id, args: [:id], get?: true Target: `Ash.Resource.Interface` -## code_interface.define_calculation +### code_interface.define_calculation ```elixir define_calculation name ``` @@ -2149,7 +2149,7 @@ end -## identities.identity +### identities.identity ```elixir identity name, keys ``` @@ -2226,7 +2226,7 @@ end -## changes.change +### changes.change ```elixir change change ``` @@ -2296,7 +2296,7 @@ end -## preparations.prepare +### preparations.prepare ```elixir prepare preparation ``` @@ -2353,7 +2353,7 @@ end -## validations.validate +### validations.validate ```elixir validate validation ``` @@ -2448,7 +2448,7 @@ end -## aggregates.count +### aggregates.count ```elixir count name, relationship_path ``` @@ -2498,7 +2498,7 @@ end | [`authorize?`](#aggregates-count-authorize?){: #aggregates-count-authorize? } | `boolean` | `true` | Whether or not the aggregate query should authorize based on the target action, if the parent query is authorized. Requires filter checks on the target action. | -## aggregates.count.join_filter +### aggregates.count.join_filter ```elixir join_filter relationship_path, filter ``` @@ -2540,7 +2540,7 @@ Target: `Ash.Resource.Aggregate.JoinFilter` Target: `Ash.Resource.Aggregate` -## aggregates.exists +### aggregates.exists ```elixir exists name, relationship_path ``` @@ -2586,7 +2586,7 @@ exists :has_ticket, :assigned_tickets | [`authorize?`](#aggregates-exists-authorize?){: #aggregates-exists-authorize? } | `boolean` | `true` | Whether or not the aggregate query should authorize based on the target action, if the parent query is authorized. Requires filter checks on the target action. | -## aggregates.exists.join_filter +### aggregates.exists.join_filter ```elixir join_filter relationship_path, filter ``` @@ -2628,7 +2628,7 @@ Target: `Ash.Resource.Aggregate.JoinFilter` Target: `Ash.Resource.Aggregate` -## aggregates.first +### aggregates.first ```elixir first name, relationship_path, field ``` @@ -2681,7 +2681,7 @@ end | [`authorize?`](#aggregates-first-authorize?){: #aggregates-first-authorize? } | `boolean` | `true` | Whether or not the aggregate query should authorize based on the target action, if the parent query is authorized. Requires filter checks on the target action. | -## aggregates.first.join_filter +### aggregates.first.join_filter ```elixir join_filter relationship_path, filter ``` @@ -2723,7 +2723,7 @@ Target: `Ash.Resource.Aggregate.JoinFilter` Target: `Ash.Resource.Aggregate` -## aggregates.sum +### aggregates.sum ```elixir sum name, relationship_path, field ``` @@ -2772,7 +2772,7 @@ end | [`authorize?`](#aggregates-sum-authorize?){: #aggregates-sum-authorize? } | `boolean` | `true` | Whether or not the aggregate query should authorize based on the target action, if the parent query is authorized. Requires filter checks on the target action. | -## aggregates.sum.join_filter +### aggregates.sum.join_filter ```elixir join_filter relationship_path, filter ``` @@ -2814,7 +2814,7 @@ Target: `Ash.Resource.Aggregate.JoinFilter` Target: `Ash.Resource.Aggregate` -## aggregates.list +### aggregates.list ```elixir list name, relationship_path, field ``` @@ -2867,7 +2867,7 @@ end | [`authorize?`](#aggregates-list-authorize?){: #aggregates-list-authorize? } | `boolean` | `true` | Whether or not the aggregate query should authorize based on the target action, if the parent query is authorized. Requires filter checks on the target action. | -## aggregates.list.join_filter +### aggregates.list.join_filter ```elixir join_filter relationship_path, filter ``` @@ -2909,7 +2909,7 @@ Target: `Ash.Resource.Aggregate.JoinFilter` Target: `Ash.Resource.Aggregate` -## aggregates.max +### aggregates.max ```elixir max name, relationship_path, field ``` @@ -2958,7 +2958,7 @@ end | [`authorize?`](#aggregates-max-authorize?){: #aggregates-max-authorize? } | `boolean` | `true` | Whether or not the aggregate query should authorize based on the target action, if the parent query is authorized. Requires filter checks on the target action. | -## aggregates.max.join_filter +### aggregates.max.join_filter ```elixir join_filter relationship_path, filter ``` @@ -3000,7 +3000,7 @@ Target: `Ash.Resource.Aggregate.JoinFilter` Target: `Ash.Resource.Aggregate` -## aggregates.min +### aggregates.min ```elixir min name, relationship_path, field ``` @@ -3049,7 +3049,7 @@ end | [`authorize?`](#aggregates-min-authorize?){: #aggregates-min-authorize? } | `boolean` | `true` | Whether or not the aggregate query should authorize based on the target action, if the parent query is authorized. Requires filter checks on the target action. | -## aggregates.min.join_filter +### aggregates.min.join_filter ```elixir join_filter relationship_path, filter ``` @@ -3091,7 +3091,7 @@ Target: `Ash.Resource.Aggregate.JoinFilter` Target: `Ash.Resource.Aggregate` -## aggregates.avg +### aggregates.avg ```elixir avg name, relationship_path, field ``` @@ -3140,7 +3140,7 @@ end | [`authorize?`](#aggregates-avg-authorize?){: #aggregates-avg-authorize? } | `boolean` | `true` | Whether or not the aggregate query should authorize based on the target action, if the parent query is authorized. Requires filter checks on the target action. | -## aggregates.avg.join_filter +### aggregates.avg.join_filter ```elixir join_filter relationship_path, filter ``` @@ -3182,7 +3182,7 @@ Target: `Ash.Resource.Aggregate.JoinFilter` Target: `Ash.Resource.Aggregate` -## aggregates.custom +### aggregates.custom ```elixir custom name, relationship_path, type ``` @@ -3236,7 +3236,7 @@ end | [`authorize?`](#aggregates-custom-authorize?){: #aggregates-custom-authorize? } | `boolean` | `true` | Whether or not the aggregate query should authorize based on the target action, if the parent query is authorized. Requires filter checks on the target action. | -## aggregates.custom.join_filter +### aggregates.custom.join_filter ```elixir join_filter relationship_path, filter ``` @@ -3306,7 +3306,7 @@ end -## calculations.calculate +### calculations.calculate ```elixir calculate name, type, calculation \\ nil ``` @@ -3379,7 +3379,7 @@ end | [`sortable?`](#calculations-calculate-sortable?){: #calculations-calculate-sortable? } | `boolean` | `true` | Whether or not the calculation can be referenced in sorts. | -## calculations.calculate.argument +### calculations.calculate.argument ```elixir argument name, type ``` diff --git a/mix.exs b/mix.exs index 027ac6d17..44679d8da 100644 --- a/mix.exs +++ b/mix.exs @@ -41,13 +41,20 @@ defmodule Ash.MixProject do extra_section: "GUIDES", extras: [ {"README.md", title: "Home"}, - "documentation/dsls/DSL-Ash.Resource.md", - "documentation/dsls/DSL-Ash.Domain.md", - "documentation/dsls/DSL-Ash.Notifier.PubSub.md", - "documentation/dsls/DSL-Ash.Policy.Authorizer.md", - "documentation/dsls/DSL-Ash.DataLayer.Ets.md", - "documentation/dsls/DSL-Ash.DataLayer.Mnesia.md", - "documentation/dsls/DSL-Ash.Reactor.md", + {"documentation/dsls/Ash.Resource.md", + search_data: Spark.Docs.search_data_for(Ash.Resource.Dsl)}, + {"documentation/dsls/Ash.Domain.md", + search_data: Spark.Docs.search_data_for(Ash.Domain.Dsl)}, + {"documentation/dsls/Ash.Notifier.PubSub.md", + search_data: Spark.Docs.search_data_for(Ash.Notifier.PubSub)}, + {"documentation/dsls/Ash.Policy.Authorizer.md", + search_data: Spark.Docs.search_data_for(Ash.Policy.Authorizer)}, + {"documentation/dsls/Ash.DataLayer.Ets.md", + search_data: Spark.Docs.search_data_for(Ash.DataLayer.Ets)}, + {"documentation/dsls/Ash.DataLayer.Mnesia.md", + search_data: Spark.Docs.search_data_for(Ash.DataLayer.Mnesia)}, + {"documentation/dsls/Ash.Reactor.md", + search_data: Spark.Docs.search_data_for(Ash.Reactor)}, "documentation/tutorials/get-started.md", "documentation/topics/about_ash/what-is-ash.md", "documentation/topics/about_ash/design-principles.md", @@ -376,7 +383,8 @@ defmodule Ash.MixProject do # Dev/Test dependencies {:eflame, "~> 1.0", only: [:dev, :test]}, - {:ex_doc, "~> 0.32", only: [:dev, :test], runtime: false}, + # {:ex_doc, "~> 0.32", only: [:dev, :test], runtime: false}, + {:ex_doc, github: "elixir-lang/ex_doc", only: [:dev, :test], runtime: false}, {:ex_check, "~> 0.12", only: [:dev, :test]}, {:credo, ">= 0.0.0", only: [:dev, :test], runtime: false}, {:dialyxir, ">= 0.0.0", only: [:dev, :test], runtime: false}, @@ -397,8 +405,7 @@ defmodule Ash.MixProject do docs: [ "spark.cheat_sheets", "docs", - "spark.replace_doc_links", - "spark.cheat_sheets_in_search" + "spark.replace_doc_links" ], format: "format --migrate", "spark.cheat_sheets_in_search": diff --git a/mix.lock b/mix.lock index 8ba5b4bb7..f233dee61 100644 --- a/mix.lock +++ b/mix.lock @@ -13,7 +13,7 @@ "erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"}, "ets": {:hex, :ets, "0.9.0", "79c6a6c205436780486f72d84230c6cba2f8a9920456750ddd1e47389107d5fd", [:mix], [], "hexpm", "2861fdfb04bcaeff370f1a5904eec864f0a56dcfebe5921ea9aadf2a481c822b"}, "ex_check": {:hex, :ex_check, "0.16.0", "07615bef493c5b8d12d5119de3914274277299c6483989e52b0f6b8358a26b5f", [:mix], [], "hexpm", "4d809b72a18d405514dda4809257d8e665ae7cf37a7aee3be6b74a34dec310f5"}, - "ex_doc": {:hex, :ex_doc, "0.36.1", "4197d034f93e0b89ec79fac56e226107824adcce8d2dd0a26f5ed3a95efc36b1", [:mix], [{:earmark_parser, "~> 1.4.42", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "d7d26a7cf965dacadcd48f9fa7b5953d7d0cfa3b44fa7a65514427da44eafd89"}, + "ex_doc": {:git, "https://github.com/elixir-lang/ex_doc.git", "d387c94a74ad395a6fd2ee41326c3bda58230510", []}, "file_system": {:hex, :file_system, "1.0.1", "79e8ceaddb0416f8b8cd02a0127bdbababe7bf4a23d2a395b983c1f8b3f73edd", [:mix], [], "hexpm", "4414d1f38863ddf9120720cd976fce5bdde8e91d8283353f0e31850fa89feb9e"}, "git_cli": {:hex, :git_cli, "0.3.0", "a5422f9b95c99483385b976f5d43f7e8233283a47cda13533d7c16131cb14df5", [:mix], [], "hexpm", "78cb952f4c86a41f4d3511f1d3ecb28edb268e3a7df278de2faa1bd4672eaf9b"}, "git_ops": {:hex, :git_ops, "2.6.3", "38c6e381b8281b86e2911fa39bea4eab2d171c86d7428786566891efb73b68c3", [:mix], [{:git_cli, "~> 0.2", [hex: :git_cli, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "a81cb6c6a2a026a4d48cb9a2e1dfca203f9283a3a70aa0c7bc171970c44f23f8"}, @@ -30,7 +30,7 @@ "mimic": {:hex, :mimic, "1.11.0", "49b126687520b6e179acab305068ad7d72bfea8abe94908a6c0c8ca0a5b7bdc7", [:mix], [{:ham, "~> 0.2", [hex: :ham, repo: "hexpm", optional: false]}], "hexpm", "8b16b1809ca947cffbaede146cd42da8c1c326af67a84b59b01c204d54e4f1a2"}, "mix_audit": {:hex, :mix_audit, "2.1.4", "0a23d5b07350cdd69001c13882a4f5fb9f90fbd4cbf2ebc190a2ee0d187ea3e9", [:make, :mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.11", [hex: :yaml_elixir, repo: "hexpm", optional: false]}], "hexpm", "fd807653cc8c1cada2911129c7eb9e985e3cc76ebf26f4dd628bb25bbcaa7099"}, "mix_test_watch": {:hex, :mix_test_watch, "1.2.0", "1f9acd9e1104f62f280e30fc2243ae5e6d8ddc2f7f4dc9bceb454b9a41c82b42", [:mix], [{:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm", "278dc955c20b3fb9a3168b5c2493c2e5cffad133548d307e0a50c7f2cfbf34f6"}, - "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, + "nimble_parsec": {:hex, :nimble_parsec, "1.4.1", "f41275a0354c736db4b1d255b5d2a27c91028e55c21ea3145b938e22649ffa3f", [:mix], [], "hexpm", "605e44204998f138d6e13be366c8e81af860e726c8177caf50067e1b618fe522"}, "owl": {:hex, :owl, "0.12.0", "0c4b48f90797a7f5f09ebd67ba7ebdc20761c3ec9c7928dfcafcb6d3c2d25c99", [:mix], [{:ucwidth, "~> 0.2", [hex: :ucwidth, repo: "hexpm", optional: true]}], "hexpm", "241d85ae62824dd72f9b2e4a5ba4e69ebb9960089a3c68ce6c1ddf2073db3c15"}, "picosat_elixir": {:hex, :picosat_elixir, "0.2.3", "bf326d0f179fbb3b706bb2c15fbc367dacfa2517157d090fdfc32edae004c597", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f76c9db2dec9d2561ffaa9be35f65403d53e984e8cd99c832383b7ab78c16c66"}, "plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"}, @@ -40,7 +40,7 @@ "simple_sat": {:hex, :simple_sat, "0.1.3", "f650fc3c184a5fe741868b5ac56dc77fdbb428468f6dbf1978e14d0334497578", [:mix], [], "hexpm", "a54305066a356b7194dc81db2a89232bacdc0b3edaef68ed9aba28dcbc34887b"}, "sobelow": {:hex, :sobelow, "0.13.0", "218afe9075904793f5c64b8837cc356e493d88fddde126a463839351870b8d1e", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "cd6e9026b85fc35d7529da14f95e85a078d9dd1907a9097b3ba6ac7ebbe34a0d"}, "sourceror": {:hex, :sourceror, "1.7.1", "599d78f4cc2be7d55c9c4fd0a8d772fd0478e3a50e726697c20d13d02aa056d4", [:mix], [], "hexpm", "cd6f268fe29fa00afbc535e215158680a0662b357dc784646d7dff28ac65a0fc"}, - "spark": {:hex, :spark, "2.2.36", "07c921e5efb27f184267c3431d2f82099e24cac90748a47383dd75cbfb558268", [:mix], [{:igniter, ">= 0.3.64 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.2", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "e5ac56b75e5ad43da6d8302b6713277488f8e9a3abdba9aae8f0d0f9cff04538"}, + "spark": {:hex, :spark, "2.2.37", "405a4e44698acb7722bec62fb609ce35fb586b4bab619f3938932fade00213c3", [:mix], [{:igniter, ">= 0.3.64 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}, {:sourceror, "~> 1.2", [hex: :sourceror, repo: "hexpm", optional: true]}], "hexpm", "f55535f683981cfdc26d7af2121fb30dd8a076a7bdb4d5846e361a397d59bc4c"}, "spitfire": {:hex, :spitfire, "0.1.4", "8fe0df66e735323e4f2a56e719603391b160dd68efd922cadfbb85a2cf6c68af", [:mix], [], "hexpm", "d40d850f4ede5235084876246756b90c7bcd12994111d57c55e2e1e23ac3fe61"}, "splode": {:hex, :splode, "0.2.7", "ed042fa9bd8fe7b66dd0a0faabdb97352058420d90cd1c7c1537f609deb7ef6d", [:mix], [], "hexpm", "267f1f51d5a5ac988cda0649498294844988c5086916fed5a8aff297d69a2059"}, "statistex": {:hex, :statistex, "1.0.0", "f3dc93f3c0c6c92e5f291704cf62b99b553253d7969e9a5fa713e5481cd858a5", [:mix], [], "hexpm", "ff9d8bee7035028ab4742ff52fc80a2aa35cece833cf5319009b52f1b5a86c27"}, From 093eb29edcbd0977284d54c124a79d26887a9b31 Mon Sep 17 00:00:00 2001 From: Zach Daniel Date: Sun, 19 Jan 2025 00:27:29 -0500 Subject: [PATCH 18/22] chore: update mix.lock --- mix.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mix.lock b/mix.lock index f233dee61..cb1f0f5c6 100644 --- a/mix.lock +++ b/mix.lock @@ -13,7 +13,7 @@ "erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"}, "ets": {:hex, :ets, "0.9.0", "79c6a6c205436780486f72d84230c6cba2f8a9920456750ddd1e47389107d5fd", [:mix], [], "hexpm", "2861fdfb04bcaeff370f1a5904eec864f0a56dcfebe5921ea9aadf2a481c822b"}, "ex_check": {:hex, :ex_check, "0.16.0", "07615bef493c5b8d12d5119de3914274277299c6483989e52b0f6b8358a26b5f", [:mix], [], "hexpm", "4d809b72a18d405514dda4809257d8e665ae7cf37a7aee3be6b74a34dec310f5"}, - "ex_doc": {:git, "https://github.com/elixir-lang/ex_doc.git", "d387c94a74ad395a6fd2ee41326c3bda58230510", []}, + "ex_doc": {:git, "https://github.com/elixir-lang/ex_doc.git", "d399df06f01bd8975bd9f0c95cce9eda065ce476", []}, "file_system": {:hex, :file_system, "1.0.1", "79e8ceaddb0416f8b8cd02a0127bdbababe7bf4a23d2a395b983c1f8b3f73edd", [:mix], [], "hexpm", "4414d1f38863ddf9120720cd976fce5bdde8e91d8283353f0e31850fa89feb9e"}, "git_cli": {:hex, :git_cli, "0.3.0", "a5422f9b95c99483385b976f5d43f7e8233283a47cda13533d7c16131cb14df5", [:mix], [], "hexpm", "78cb952f4c86a41f4d3511f1d3ecb28edb268e3a7df278de2faa1bd4672eaf9b"}, "git_ops": {:hex, :git_ops, "2.6.3", "38c6e381b8281b86e2911fa39bea4eab2d171c86d7428786566891efb73b68c3", [:mix], [{:git_cli, "~> 0.2", [hex: :git_cli, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "a81cb6c6a2a026a4d48cb9a2e1dfca203f9283a3a70aa0c7bc171970c44f23f8"}, @@ -40,7 +40,7 @@ "simple_sat": {:hex, :simple_sat, "0.1.3", "f650fc3c184a5fe741868b5ac56dc77fdbb428468f6dbf1978e14d0334497578", [:mix], [], "hexpm", "a54305066a356b7194dc81db2a89232bacdc0b3edaef68ed9aba28dcbc34887b"}, "sobelow": {:hex, :sobelow, "0.13.0", "218afe9075904793f5c64b8837cc356e493d88fddde126a463839351870b8d1e", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "cd6e9026b85fc35d7529da14f95e85a078d9dd1907a9097b3ba6ac7ebbe34a0d"}, "sourceror": {:hex, :sourceror, "1.7.1", "599d78f4cc2be7d55c9c4fd0a8d772fd0478e3a50e726697c20d13d02aa056d4", [:mix], [], "hexpm", "cd6f268fe29fa00afbc535e215158680a0662b357dc784646d7dff28ac65a0fc"}, - "spark": {:hex, :spark, "2.2.37", "405a4e44698acb7722bec62fb609ce35fb586b4bab619f3938932fade00213c3", [:mix], [{:igniter, ">= 0.3.64 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}, {:sourceror, "~> 1.2", [hex: :sourceror, repo: "hexpm", optional: true]}], "hexpm", "f55535f683981cfdc26d7af2121fb30dd8a076a7bdb4d5846e361a397d59bc4c"}, + "spark": {:hex, :spark, "2.2.37", "2b0878d417ea98b241742c56fe615f238ae5df11a231f5a622d8266ffac661c3", [:mix], [{:igniter, ">= 0.3.64 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}, {:sourceror, "~> 1.2", [hex: :sourceror, repo: "hexpm", optional: true]}], "hexpm", "0f62dfc2ff5b410070e2b6b84188f8be92f34641875e20f4765da34842e42dca"}, "spitfire": {:hex, :spitfire, "0.1.4", "8fe0df66e735323e4f2a56e719603391b160dd68efd922cadfbb85a2cf6c68af", [:mix], [], "hexpm", "d40d850f4ede5235084876246756b90c7bcd12994111d57c55e2e1e23ac3fe61"}, "splode": {:hex, :splode, "0.2.7", "ed042fa9bd8fe7b66dd0a0faabdb97352058420d90cd1c7c1537f609deb7ef6d", [:mix], [], "hexpm", "267f1f51d5a5ac988cda0649498294844988c5086916fed5a8aff297d69a2059"}, "statistex": {:hex, :statistex, "1.0.0", "f3dc93f3c0c6c92e5f291704cf62b99b553253d7969e9a5fa713e5481cd858a5", [:mix], [], "hexpm", "ff9d8bee7035028ab4742ff52fc80a2aa35cece833cf5319009b52f1b5a86c27"}, From 035d010919ac58e82c3b80bb7c35b851f036563b Mon Sep 17 00:00:00 2001 From: Zach Daniel Date: Mon, 20 Jan 2025 14:37:50 -0500 Subject: [PATCH 19/22] improvement: add `uses` option for `changeset_generator` improvement: add `uses` option for `seed_generator` See the added docs for more. This aids in writing generators where multiple fields have requirements on some other given generator. --- lib/ash/generator/generator.ex | 146 +++++++++++++++++++++++++----- test/generator/generator_test.exs | 47 ++++++++++ 2 files changed, 170 insertions(+), 23 deletions(-) diff --git a/lib/ash/generator/generator.ex b/lib/ash/generator/generator.ex index 495e962cf..672c23201 100644 --- a/lib/ash/generator/generator.ex +++ b/lib/ash/generator/generator.ex @@ -158,32 +158,95 @@ defmodule Ash.Generator do ## Options + * `:defaults` - A keyword list of values or generators, used as inputs. Can also be a function + when using the `:uses` option. * `:overrides` - A keyword list or map of `t:overrides()` * `:actor` - Passed through to the changeset * `:tenant` - Passed through to the changeset + * `:uses` - A map of generators that are passed into your `defaults`. `defaults` must be a + function. This is useful when multiple things in your `defaults` need to use the same generated + value. * `:authorize?` - Passed through to the changeset * `:context` - Passed through to the changeset * `:after_action` - A one argument function that takes the result and returns a new result to run after the record is creatd. + + ## The `uses` option + + ```elixir + def blog_post(opts \\ []) do + changeset_generator( + MyApp.Blog.Post, + :create, + uses: [ + author: author() # A function using `changeset_generator` just like this one. + ], + defaults: fn %{author: author} -> + author = generate(author) + + [ + name: sequence(:blog_post_title, &"My Blog Post \#{&1}") + author_name: author.name, + text: StreamData.repeatedly(fn -> Faker.Lorem.paragraph() end) + ] + end + overrides: opts + ) + end + ``` + """ def changeset_generator(resource, action, opts \\ []) do - generators = - opts[:defaults] - |> Kernel.||([]) - |> Map.new() - |> Map.merge(Map.new(opts[:overrides] || %{})) - changeset_opts = StreamData.fixed_map( to_generators(Keyword.take(opts, [:actor, :tenant, :authorize?, :context])) ) - input = action_input(resource, action, generators) + generator = + if opts[:uses] do + if !is_function(opts[:defaults]) do + raise ArgumentError, + "The `uses` option can only be provided if the `defaults` option is a function" + end - StreamData.fixed_map(%{ - changeset_opts: changeset_opts, - input: input - }) + opts[:uses] + |> to_generators() + |> StreamData.fixed_map() + |> StreamData.bind(fn uses -> + generators = + opts[:defaults].(uses) + |> Map.new() + |> Map.merge(Map.new(opts[:overrides] || %{})) + + action_input(resource, action, generators) + end) + |> StreamData.bind(fn input -> + StreamData.fixed_map(%{ + changeset_opts: changeset_opts, + input: StreamData.fixed_map(to_generators(input)) + }) + end) + else + if is_function(opts[:defaults]) do + raise ArgumentError, + "The `uses` option must be provided if the `defaults` option is a function" + end + + generators = + opts[:defaults] + |> Kernel.||([]) + |> Map.new() + |> Map.merge(Map.new(opts[:overrides] || %{})) + + input = action_input(resource, action, generators) + + StreamData.fixed_map(%{ + changeset_opts: changeset_opts, + input: input + }) + end + + generator |> StreamData.map(fn %{input: input, changeset_opts: changeset_opts} -> Ash.Changeset.for_action( resource, @@ -240,23 +303,60 @@ defmodule Ash.Generator do * `:overrides` - A keyword list or map of `t:overrides()` * `:actor` - Passed through to the changeset * `:tenant` - Passed through to the changeset + * `:uses` - A map of generators that are passed into the first argument, if it is a function. * `:authorize?` - Passed through to the changeset * `:context` - Passed through to the changeset * `:after_action` - A one argument function that takes the result and returns a new result to run after the record is creatd. """ - @spec seed_generator(Ash.Resource.record(), opts :: Keyword.t()) :: stream_data() - def seed_generator(%resource{} = record, opts \\ []) do - record - |> Map.take(Enum.to_list(Ash.Resource.Info.attribute_names(resource))) - |> Map.merge(Map.new(opts[:overrides] || %{})) - |> to_generators() - |> StreamData.fixed_map() - |> StreamData.map(fn keys -> - Ash.Resource.set_metadata(struct(resource, keys), %{ - private: %{generator_after_action: opts[:after_action]} - }) - end) + @spec seed_generator( + Ash.Resource.record() | (map -> Ash.Resource.record()), + opts :: Keyword.t() + ) :: stream_data() + def seed_generator(record, opts \\ []) do + if opts[:uses] do + if !is_function(record) do + raise ArgumentError, "The `uses` option can only be provided if `record` is a function" + end + + opts[:uses] + |> to_generators() + |> StreamData.fixed_map() + |> StreamData.bind(fn uses -> + %resource{} = + record = + record.(uses) + + record + |> Map.take(Enum.to_list(Ash.Resource.Info.attribute_names(resource))) + |> Map.merge(Map.new(opts[:overrides] || %{})) + |> to_generators() + |> Map.put(:__will_be_struct__, resource) + |> StreamData.fixed_map() + end) + |> StreamData.map(fn keys -> + Ash.Resource.set_metadata(struct(keys.__will_be_struct__, keys), %{ + private: %{generator_after_action: opts[:after_action]} + }) + end) + else + if is_function(record) do + raise ArgumentError, "The `uses` option must be provided if `record` is a function" + end + + %resource{} = record + + record + |> Map.take(Enum.to_list(Ash.Resource.Info.attribute_names(resource))) + |> Map.merge(Map.new(opts[:overrides] || %{})) + |> to_generators() + |> StreamData.fixed_map() + |> StreamData.map(fn keys -> + Ash.Resource.set_metadata(struct(resource, keys), %{ + private: %{generator_after_action: opts[:after_action]} + }) + end) + end end @doc """ diff --git a/test/generator/generator_test.exs b/test/generator/generator_test.exs index 2bc7b3be1..02bc9a50f 100644 --- a/test/generator/generator_test.exs +++ b/test/generator/generator_test.exs @@ -89,6 +89,10 @@ defmodule Ash.Test.GeneratorTest do public?(true) end + attribute :title_again, :string do + public?(true) + end + attribute :contents, :string do public?(true) end @@ -224,6 +228,33 @@ defmodule Ash.Test.GeneratorTest do ) end + def post_with_double_name(opts \\ []) do + changeset_generator(Post, :create, + uses: %{ + name: sequence(:title, &"Post #{&1}") + }, + defaults: fn uses -> + [title: uses.name, title_again: uses.name] + end, + overrides: opts + ) + end + + def seed_post_with_double_name(opts \\ []) do + seed_generator( + fn uses -> + %Post{ + title: uses.name, + title_again: uses.name + } + end, + uses: %{ + name: sequence(:title, &"Post #{&1}") + }, + overrides: opts + ) + end + def embedded(opts \\ []) do changeset_generator(Embedded, :create, defaults: [ @@ -251,6 +282,22 @@ defmodule Ash.Test.GeneratorTest do assert [%Post{title: "Post 2"}, %Post{title: "Post 3"}] = generate_many(seed_post(), 2) end + test "can share generators with `uses`" do + import Generator + + assert [ + %Post{title: "Post 0", title_again: "Post 0"}, + %Post{title: "Post 1", title_again: "Post 1"} + ] = + generate_many(post_with_double_name(), 2) + + assert [ + %Post{title: "Post 2", title_again: "Post 2"}, + %Post{title: "Post 3", title_again: "Post 3"} + ] = + generate_many(seed_post_with_double_name(), 2) + end + test "errors are raised when generating invalid single items" do import Generator From cab79bd566aa31a838c5a41672809e7e1faef099 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Jan 2025 17:48:18 -0500 Subject: [PATCH 20/22] chore(deps): bump igniter in the production-dependencies group (#1723) Bumps the production-dependencies group with 1 update: [igniter](https://github.com/ash-project/igniter). Updates `igniter` from 0.5.8 to 0.5.11 - [Changelog](https://github.com/ash-project/igniter/blob/main/CHANGELOG.md) - [Commits](https://github.com/ash-project/igniter/compare/v0.5.8...v0.5.11) --- updated-dependencies: - dependency-name: igniter dependency-type: direct:production update-type: version-update:semver-patch dependency-group: production-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.lock b/mix.lock index cb1f0f5c6..0171899cc 100644 --- a/mix.lock +++ b/mix.lock @@ -19,7 +19,7 @@ "git_ops": {:hex, :git_ops, "2.6.3", "38c6e381b8281b86e2911fa39bea4eab2d171c86d7428786566891efb73b68c3", [:mix], [{:git_cli, "~> 0.2", [hex: :git_cli, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "a81cb6c6a2a026a4d48cb9a2e1dfca203f9283a3a70aa0c7bc171970c44f23f8"}, "glob_ex": {:hex, :glob_ex, "0.1.11", "cb50d3f1ef53f6ca04d6252c7fde09fd7a1cf63387714fe96f340a1349e62c93", [:mix], [], "hexpm", "342729363056e3145e61766b416769984c329e4378f1d558b63e341020525de4"}, "ham": {:hex, :ham, "0.3.0", "7cd031b4a55fba219c11553e7b13ba73bd86eab4034518445eff1e038cb9a44d", [:mix], [], "hexpm", "7d6c6b73d7a6a83233876cc1b06a4d9b5de05562b228effda4532f9a49852bf6"}, - "igniter": {:hex, :igniter, "0.5.8", "d91e90fecb99beadfa9d0d8434fbd4f0fe06ea1a1d29cae4dfd0cb058cb3a5c7", [:mix], [{:glob_ex, "~> 0.1.7", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:rewrite, ">= 1.1.1 and < 2.0.0-0", [hex: :rewrite, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.4", [hex: :sourceror, repo: "hexpm", optional: false]}, {:spitfire, ">= 0.1.3 and < 1.0.0-0", [hex: :spitfire, repo: "hexpm", optional: false]}], "hexpm", "fef198324925405ea5c3b16166002be03b2d7497c038cfc9708aa557d27ba5a2"}, + "igniter": {:hex, :igniter, "0.5.11", "40a77910ddd3b81e58bc6c0411f2b4a1d2102bc1dd81818da6b7fceacfe1c0c6", [:mix], [{:glob_ex, "~> 0.1.7", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:phx_new, "~> 1.7", [hex: :phx_new, repo: "hexpm", optional: true]}, {:rewrite, ">= 1.1.1 and < 2.0.0-0", [hex: :rewrite, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.4", [hex: :sourceror, repo: "hexpm", optional: false]}, {:spitfire, ">= 0.1.3 and < 1.0.0-0", [hex: :spitfire, repo: "hexpm", optional: false]}], "hexpm", "d41ef6bbed10bbf324d83473b818fa1367b5eb244eca05dfb3ea862e8a0c1567"}, "iterex": {:hex, :iterex, "0.1.2", "58f9b9b9a22a55cbfc7b5234a9c9c63eaac26d276b3db80936c0e1c60355a5a6", [:mix], [], "hexpm", "2e103b8bcc81757a9af121f6dc0df312c9a17220f302b1193ef720460d03029d"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, "libgraph": {:hex, :libgraph, "0.16.0", "3936f3eca6ef826e08880230f806bfea13193e49bf153f93edcf0239d4fd1d07", [:mix], [], "hexpm", "41ca92240e8a4138c30a7e06466acc709b0cbb795c643e9e17174a178982d6bf"}, From 016d2488bc39c351fc0db100da6aa4ad78d97c50 Mon Sep 17 00:00:00 2001 From: Zach Daniel Date: Tue, 21 Jan 2025 10:16:51 -0500 Subject: [PATCH 21/22] improvement: support error shorthand for `Ash.Query.add_error/2-3` --- lib/ash/query/query.ex | 56 +++++++++++++++++++++++++++++++++++------- 1 file changed, 47 insertions(+), 9 deletions(-) diff --git a/lib/ash/query/query.ex b/lib/ash/query/query.ex index 3b4838114..e28f21d04 100644 --- a/lib/ash/query/query.ex +++ b/lib/ash/query/query.ex @@ -123,6 +123,7 @@ defmodule Ash.Query do InvalidLimit, InvalidOffset, InvalidPage, + InvalidQuery, NoReadAction, ReadActionRequiresActor, Required @@ -3090,19 +3091,56 @@ defmodule Ash.Query do """ @spec add_error(t(), path :: Ash.Error.path_input(), Ash.Error.error_input()) :: t() @spec add_error(t(), Ash.Error.error_input()) :: t() - def add_error(query, path \\ [], error) do + + def add_error(query, path \\ [], error) + + def add_error(query, path, errors) when is_list(errors) do + if Keyword.keyword?(errors) do + error = + errors + |> to_query_error() + |> Ash.Error.set_path(path) + + add_error(query, error) + else + Enum.reduce(errors, query, &add_error(&2, &1, path)) + end + end + + def add_error(query, path, error) do path = List.wrap(path) query = new(query) - error - |> Ash.Error.to_ash_error() - |> Ash.Error.set_path(path) - |> case do - errors when is_list(errors) -> - %{query | errors: query.errors ++ errors, valid?: false} + error = + error + |> Ash.Error.to_ash_error() + |> Ash.Error.set_path(path) - error -> - %{query | errors: [error | query.errors], valid?: false} + %{query | errors: [error | query.errors], valid?: false} + end + + defp to_query_error(keyword) do + error = + if keyword[:field] do + InvalidArgument.exception( + field: keyword[:field], + message: keyword[:message], + value: keyword[:value], + vars: keyword + ) + else + InvalidQuery.exception( + fields: keyword[:fields] || [], + message: keyword[:message], + value: keyword[:value], + vars: keyword + ) + end + + if keyword[:path] do + Ash.Error.set_path(error, keyword[:path]) + else + error end end From 31ef368fa6a868a73bbe3fe3e1ecb3ed8d60d371 Mon Sep 17 00:00:00 2001 From: Zach Daniel Date: Tue, 21 Jan 2025 10:19:25 -0500 Subject: [PATCH 22/22] chore: release version 3.4.56 --- CHANGELOG.md | 39 +++++++++++++++++++++++++++++++++++++++ mix.exs | 2 +- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 428274722..3de6d4a5b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,45 @@ +## [3.4.56](https://github.com/ash-project/ash/compare/v3.4.55...3.4.56) (2025-01-21) + + + + +### Features: + +* make atomics work even if expr err is not supported (#1718) + +### Bug Fixes: + +* properly load doubly nested calculation's explicit dependencies + +* use `Jason` always for compatibility + +* handle related non-expr calculations referenced from expr calcs + +* matching in managed_relationships handle_update (#1719) + +* simplify and fix path generation for nested relationship path deps + +* don’t require multitenancy attribute in `get` (#1716) + +### Improvements: + +* support error shorthand for `Ash.Query.add_error/2-3` + +* add `uses` option for `changeset_generator` + +* add `uses` option for `seed_generator` + +* Use clearer error message for match validation atomic errors (#1721) + +* Add autogenerate_enabled? to Ash.Type for Ecto compatability (#1715) + +* warn when domain policies would be ignored by resources + +* allow policy authorizer to be in authorizers key in domains + ## [v3.4.55](https://github.com/ash-project/ash/compare/v3.4.54...v3.4.55) (2025-01-13) diff --git a/mix.exs b/mix.exs index 44679d8da..b7098a7d0 100644 --- a/mix.exs +++ b/mix.exs @@ -6,7 +6,7 @@ defmodule Ash.MixProject do A declarative, extensible framework for building Elixir applications. """ - @version "3.4.55" + @version "3.4.56" def project do [