From 5fc5d32508426304ea8ea876a7438d4d4f0933c1 Mon Sep 17 00:00:00 2001 From: Zach Daniel Date: Mon, 19 Aug 2024 21:49:34 -0400 Subject: [PATCH] improvement: cache action known inputs individually improvement: cache action required inputs all together --- .formatter.exs | 1 + documentation/dsls/DSL:-Ash.Resource.md | 3 +- lib/ash/changeset/changeset.ex | 26 ++----------- lib/ash/resource/info.ex | 10 +++++ .../transformers/cache_action_inputs.ex | 38 +++++++++++++++++-- mix.exs | 2 +- mix.lock | 4 +- 7 files changed, 55 insertions(+), 29 deletions(-) diff --git a/.formatter.exs b/.formatter.exs index 1a9dc78a..eab2e7cd 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -15,6 +15,7 @@ spark_locals_without_parens = [ args: 1, argument: 2, argument: 3, + async?: 1, atomic_upgrade?: 1, atomic_upgrade_with: 1, attribute: 1, diff --git a/documentation/dsls/DSL:-Ash.Resource.md b/documentation/dsls/DSL:-Ash.Resource.md index 86088875..3c2abf6b 100644 --- a/documentation/dsls/DSL:-Ash.Resource.md +++ b/documentation/dsls/DSL:-Ash.Resource.md @@ -95,7 +95,7 @@ end | [`generated?`](#attributes-attribute-generated?){: #attributes-attribute-generated? } | `boolean` | `false` | Whether or not the value may be generated by the data layer. | | [`writable?`](#attributes-attribute-writable?){: #attributes-attribute-writable? } | `boolean` | `true` | Whether or not the value can be written to. Non-writable attributes can still be written with `Ash.Changeset.force_change_attribute/3`. | | [`public?`](#attributes-attribute-public?){: #attributes-attribute-public? } | `boolean` | `false` | Whether or not the attribute should be shown over public interfaces. See the [sensitive data guide](/documentation/topics/security/sensitive-data.md) for more. | -| [`default`](#attributes-attribute-default){: #attributes-attribute-default } | `(-> any) \| mfa \| any` | | A value to be set on all creates, unless a value is being provided already. Note: The default value is casted according to the type's Ash.Type.* module, before it is saved. For `:string`, for example, if `constraints: [allow_empty?: _]` is false, the value `""` will be cast to `nil`. See the `:constraints` option, the `:allow_nil?` option, and the relevant `Ash.Type.*` documentation. | +| [`default`](#attributes-attribute-default){: #attributes-attribute-default } | `(-> any) \| mfa \| any` | | A value to be set on all creates, unless a value is being provided already. Note: The default value is casted according to the type's `Ash.Type.*` module, before it is saved. For `:string`, for example, if `constraints: [allow_empty?: _]` is false, the value `""` will be cast to `nil`. See the `:constraints` option, the `:allow_nil?` option, and the relevant `Ash.Type.*` documentation. | | [`update_default`](#attributes-attribute-update_default){: #attributes-attribute-update_default } | `(-> any) \| mfa \| any` | | A value to be set on all updates, unless a value is being provided already. | | [`filterable?`](#attributes-attribute-filterable?){: #attributes-attribute-filterable? } | `boolean \| :simple_equality` | `true` | Whether or not the attribute can be referenced in filters. | | [`sortable?`](#attributes-attribute-sortable?){: #attributes-attribute-sortable? } | `boolean` | `true` | Whether or not the attribute can be referenced in sorts. | @@ -3340,6 +3340,7 @@ calculate :full_name, :string, expr(first_name <> " " <> last_name) | Name | Type | Default | Docs | |------|------|---------|------| +| [`async?`](#calculations-calculate-async?){: #calculations-calculate-async? } | `boolean` | `false` | | | [`constraints`](#calculations-calculate-constraints){: #calculations-calculate-constraints } | `keyword` | `[]` | Constraints to provide to the type. See `Ash.Type` for more. | | [`description`](#calculations-calculate-description){: #calculations-calculate-description } | `String.t` | | An optional description for the calculation | | [`public?`](#calculations-calculate-public?){: #calculations-calculate-public? } | `boolean` | `false` | Whether or not the calculation will appear in public interfaces. | diff --git a/lib/ash/changeset/changeset.ex b/lib/ash/changeset/changeset.ex index 3dea16be..2c7d18e1 100644 --- a/lib/ash/changeset/changeset.ex +++ b/lib/ash/changeset/changeset.ex @@ -3117,30 +3117,12 @@ defmodule Ash.Changeset do # Attributes that are private and/or are the source field of a belongs_to relationship # are typically not set by input, so they aren't required until the actual action # is run. - defp attributes_to_require(resource, _action, true) do - resource - |> Ash.Resource.Info.attributes() - |> Enum.reject(&(&1.allow_nil? || &1.generated?)) + defp attributes_to_require(resource, _action, true = _final?) do + Ash.Resource.Info.attributes_to_require(resource) end - defp attributes_to_require(resource, action, false) do - accept = action.accept - require_attributes = action.require_attributes - allow_nil_input = action.allow_nil_input - argument_names = action.arguments |> Enum.map(& &1.name) - - accepted = - accept - |> Kernel.++(require_attributes) - |> Kernel.--(allow_nil_input) - |> Kernel.--(argument_names) - - resource - |> Ash.Resource.Info.attributes() - |> Enum.reject( - &(&1.name not in accepted || !&1.writable? || &1.generated? || - (&1.allow_nil? && &1.name not in require_attributes)) - ) + defp attributes_to_require(resource, action, false = _final?) do + Ash.Resource.Info.attributes_to_require(resource, action.name) end @doc """ diff --git a/lib/ash/resource/info.ex b/lib/ash/resource/info.ex index 9ae6ea6f..1e1209af 100644 --- a/lib/ash/resource/info.ex +++ b/lib/ash/resource/info.ex @@ -351,6 +351,14 @@ defmodule Ash.Resource.Info do ) end + def attributes_to_require(resource) do + Extension.get_persisted(resource, :attributes_to_require) + end + + def attributes_to_require(resource, action_name) do + Extension.get_persisted(resource, {:attributes_to_require, action_name}) + end + @doc "Get a public relationship by name or path" def public_relationship(resource, [name]) do public_relationship(resource, name) @@ -617,6 +625,8 @@ defmodule Ash.Resource.Info do @spec action_input?(Ash.Resource.t(), action :: atom(), input :: atom() | String.t()) :: boolean() def action_input?(resource, action, input) do + # Extension.get_persisted(resource, {:action_inputs, action, input}) || false + case Extension.get_persisted(resource, {:action_inputs, action}) do nil -> false map_set -> input in map_set diff --git a/lib/ash/resource/transformers/cache_action_inputs.ex b/lib/ash/resource/transformers/cache_action_inputs.ex index 32960dac..003e5d19 100644 --- a/lib/ash/resource/transformers/cache_action_inputs.ex +++ b/lib/ash/resource/transformers/cache_action_inputs.ex @@ -5,18 +5,50 @@ defmodule Ash.Resource.Transformers.CacheActionInputs do alias Spark.Dsl.Transformer def transform(dsl_state) do + final_attributes_to_require = + dsl_state + |> Ash.Resource.Info.attributes() + |> Enum.reject(&(&1.allow_nil? || &1.generated?)) + + dsl_state = + Transformer.persist(dsl_state, :attributes_to_require, final_attributes_to_require) + dsl_state |> Ash.Resource.Info.actions() |> Enum.reject(&(&1.type in [:read, :action])) - |> Enum.reduce({:ok, dsl_state}, fn action, {:ok, dsl_state} -> + |> Enum.reduce(dsl_state, fn action, dsl_state -> inputs = action.arguments |> Enum.map(& &1.name) |> Enum.concat(action.accept) |> Enum.flat_map(&[&1, to_string(&1)]) - |> MapSet.new() - {:ok, Transformer.persist(dsl_state, {:action_inputs, action.name}, inputs)} + argument_names = action.arguments |> Enum.map(& &1.name) + + accepted = + action.accept + |> Kernel.++(action.require_attributes) + |> Kernel.--(action.allow_nil_input) + |> Kernel.--(argument_names) + + attributes_to_require_for_action = + dsl_state + |> Ash.Resource.Info.attributes() + |> Enum.reject( + &(&1.name not in accepted || !&1.writable? || &1.generated? || + (&1.allow_nil? && &1.name not in action.require_attributes)) + ) + + inputs + |> Enum.reduce(dsl_state, fn input, dsl_state -> + Transformer.persist(dsl_state, {:action_inputs, action.name, input}, true) + end) + |> Transformer.persist({:action_inputs, action.name}, MapSet.new(inputs)) + |> Transformer.persist( + {:attributes_to_require, action.name}, + attributes_to_require_for_action + ) end) + |> then(&{:ok, &1}) end end diff --git a/mix.exs b/mix.exs index c6be20a6..5f08f900 100644 --- a/mix.exs +++ b/mix.exs @@ -336,7 +336,7 @@ defmodule Ash.MixProject do defp deps do [ # DSLs - {:spark, "~> 2.1 and >= 2.2.19"}, + {:spark, "~> 2.1 and >= 2.2.22"}, # Ash resources are backed by ecto scheams {:ecto, "~> 3.7"}, # Used by the ETS data layer diff --git a/mix.lock b/mix.lock index d5187922..c2565de4 100644 --- a/mix.lock +++ b/mix.lock @@ -18,7 +18,7 @@ "git_cli": {:hex, :git_cli, "0.3.0", "a5422f9b95c99483385b976f5d43f7e8233283a47cda13533d7c16131cb14df5", [:mix], [], "hexpm", "78cb952f4c86a41f4d3511f1d3ecb28edb268e3a7df278de2faa1bd4672eaf9b"}, "git_ops": {:hex, :git_ops, "2.6.1", "cc7799a68c26cf814d6d1a5121415b4f5bf813de200908f930b27a2f1fe9dad5", [:mix], [{:git_cli, "~> 0.2", [hex: :git_cli, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "ce62d07e41fe993ec22c35d5edb11cf333a21ddaead6f5d9868fcb607d42039e"}, "glob_ex": {:hex, :glob_ex, "0.1.8", "f7ef872877ca2ae7a792ab1f9ff73d9c16bf46ecb028603a8a3c5283016adc07", [:mix], [], "hexpm", "9e39d01729419a60a937c9260a43981440c43aa4cadd1fa6672fecd58241c464"}, - "igniter": {:hex, :igniter, "0.3.19", "dfa4a05e94be72e9b75c9ce73b9ce408c9a5fe8f1f010eb36e711e70da698b46", [:mix], [{:glob_ex, "~> 0.1.7", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:inflex, "~> 2.0", [hex: :inflex, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:nimble_options, "~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:owl, "~> 0.9", [hex: :owl, repo: "hexpm", optional: false]}, {:rewrite, "~> 0.9", [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]}, {:ucwidth, "~> 0.2", [hex: :ucwidth, repo: "hexpm", optional: false]}], "hexpm", "f9b1ca6d11a7dcd1beb0bc0a1c08b8ce1fb4eb140a763037f3b79d9170cbf958"}, + "igniter": {:hex, :igniter, "0.3.20", "2af4b7f20ba21d3f2c304b51b4b337dc9d971cd2325932758db88ddaa7db308a", [:mix], [{:glob_ex, "~> 0.1.7", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:inflex, "~> 2.0", [hex: :inflex, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:nimble_options, "~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:owl, "~> 0.9", [hex: :owl, repo: "hexpm", optional: false]}, {:rewrite, "~> 0.9", [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]}, {:ucwidth, "~> 0.2", [hex: :ucwidth, repo: "hexpm", optional: false]}], "hexpm", "8066b23e120b676c57cb2b6a1a46bb2a4a188476badeebfe6283faadd58a78c1"}, "inflex": {:hex, :inflex, "2.1.0", "a365cf0821a9dacb65067abd95008ca1b0bb7dcdd85ae59965deef2aa062924c", [:mix], [], "hexpm", "14c17d05db4ee9b6d319b0bff1bdf22aa389a25398d1952c7a0b5f3d93162dd8"}, "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"}, @@ -41,7 +41,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.6.0", "9907884e1449a4bd7dbaabe95088ed4d9a09c3c791fb0103964e6316bc9448a7", [:mix], [], "hexpm", "e90aef8c82dacf32c89c8ef83d1416fc343cd3e5556773eeffd2c1e3f991f699"}, - "spark": {:hex, :spark, "2.2.21", "b343f3488b5a986ad38d15ac124b9111b8f63f953e3a6ef3fad44fe129b7fad6", [:mix], [{:igniter, ">= 0.2.6 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.2", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "45b05c52a1afe4858e10b5e6b8cd33bff3cf0098afc144146c5ff05002d75a9d"}, + "spark": {:hex, :spark, "2.2.22", "abb5ba74ed8b8a69f8d3112fe0d74a1dea261664d9a3bcaf2d0f94f9ee7102f6", [:mix], [{:igniter, ">= 0.2.6 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.2", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "98b6ea8c19fe97b2b7b20be034ae6cf34e98b03ecba8b7d5a4cc2449f60f3f5e"}, "spitfire": {:hex, :spitfire, "0.1.3", "7ea0f544005dfbe48e615ed90250c9a271bfe126914012023fd5e4b6b82b7ec7", [:mix], [], "hexpm", "d53b5107bcff526a05c5bb54c95e77b36834550affd5830c9f58760e8c543657"}, "splode": {:hex, :splode, "0.2.4", "71046334c39605095ca4bed5d008372e56454060997da14f9868534c17b84b53", [:mix], [], "hexpm", "ca3b95f0d8d4b482b5357954fec857abd0fa3ea509d623334c1328e7382044c2"}, "statistex": {:hex, :statistex, "1.0.0", "f3dc93f3c0c6c92e5f291704cf62b99b553253d7969e9a5fa713e5481cd858a5", [:mix], [], "hexpm", "ff9d8bee7035028ab4742ff52fc80a2aa35cece833cf5319009b52f1b5a86c27"},