diff --git a/.formatter.exs b/.formatter.exs index a28f3bde..9b550ba5 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -44,6 +44,7 @@ spark_locals_without_parens = [ hide_inputs: 1, identity: 1, ignore?: 1, + include_non_field_calculations: 1, keyset_field: 1, list: 2, list: 3, diff --git a/documentation/dsls/DSL-AshGraphql.Resource.md b/documentation/dsls/DSL-AshGraphql.Resource.md index 028cf070..43e7f8f5 100644 --- a/documentation/dsls/DSL-AshGraphql.Resource.md +++ b/documentation/dsls/DSL-AshGraphql.Resource.md @@ -72,6 +72,7 @@ end | [`field_names`](#graphql-field_names){: #graphql-field_names } | `keyword` | | A keyword list of name overrides for attributes. | | [`hide_fields`](#graphql-hide_fields){: #graphql-hide_fields } | `list(atom)` | | A list of attributes to hide from the domain | | [`show_fields`](#graphql-show_fields){: #graphql-show_fields } | `list(atom)` | | A list of attributes to show in the domain. If not specified includes all (excluding `hide_fiels`). | +| [`include_non_field_calculations`](#graphql-include_non_field_calculations){: #graphql-include_non_field_calculations } | `list(atom)` | `[]` | A list of `field?: false` calculations to expose as GraphQL response fields. | | [`argument_names`](#graphql-argument_names){: #graphql-argument_names } | `keyword` | | A nested keyword list of action names, to argument name remappings. i.e `create: [arg_name: :new_name]` | | [`keyset_field`](#graphql-keyset_field){: #graphql-keyset_field } | `atom` | | If set, the keyset will be displayed on all read actions in this field. It will be `nil` unless at least one of the read actions on a resource uses keyset pagination or it is the result of a mutation | | [`attribute_types`](#graphql-attribute_types){: #graphql-attribute_types } | `keyword` | | A keyword list of type overrides for attributes. The type overrides should refer to types available in the graphql (absinthe) schema. `list_of/1` and `non_null/1` helpers can be used. | diff --git a/lib/resource/info.ex b/lib/resource/info.ex index 5e9e0e87..0b6f322d 100644 --- a/lib/resource/info.ex +++ b/lib/resource/info.ex @@ -185,6 +185,17 @@ defmodule AshGraphql.Resource.Info do Extension.get_opt(resource, [:graphql], :show_fields, nil) end + @doc "`field?: false` calculations to expose as GraphQL response fields" + def include_non_field_calculations(resource) do + Extension.get_opt(resource, [:graphql], :include_non_field_calculations, []) + end + + @doc "Whether a calculation should be exposed as a GraphQL response field" + def graphql_calculation_field?(resource, calculation) do + Map.get(calculation, :field?, true) or + calculation.name in include_non_field_calculations(resource) + end + @doc "Wether or not a given field will be shown" def show_field?(resource, field) do hide_fields = hide_fields(resource) diff --git a/lib/resource/resource.ex b/lib/resource/resource.ex index 19760d5b..041f67fb 100644 --- a/lib/resource/resource.ex +++ b/lib/resource/resource.ex @@ -474,6 +474,11 @@ defmodule AshGraphql.Resource do doc: "A list of attributes to show in the domain. If not specified includes all (excluding `hide_fiels`)." ], + include_non_field_calculations: [ + type: {:list, :atom}, + default: [], + doc: "A list of `field?: false` calculations to expose as GraphQL response fields." + ], argument_names: [ type: :keyword_list, doc: @@ -5192,7 +5197,10 @@ defmodule AshGraphql.Resource do resource |> Ash.Resource.Info.public_calculations() - |> Enum.filter(&AshGraphql.Resource.Info.show_field?(resource, &1.name)) + |> Enum.filter( + &(AshGraphql.Resource.Info.show_field?(resource, &1.name) and + AshGraphql.Resource.Info.graphql_calculation_field?(resource, &1)) + ) |> Enum.map(fn calculation -> name = field_names[calculation.name] || calculation.name field_type = calculation_type(calculation, resource) diff --git a/test/filter_sort_test.exs b/test/filter_sort_test.exs index b2857495..dcf5ee88 100644 --- a/test/filter_sort_test.exs +++ b/test/filter_sort_test.exs @@ -155,4 +155,38 @@ defmodule AshGraphql.FilterSortTest do assert sort_fields |> Enum.find(fn field -> field["name"] == "POPULARITY" end) refute sort_fields |> Enum.find(fn field -> field["name"] == "NAME" end) end + + test "field?: false calculations are included in filter input" do + resp = + """ + query { + __type(name: "PostFilterInput") { + inputFields { + name + } + } + } + """ + |> Absinthe.run(AshGraphql.Test.Schema) + + assert {:ok, %{data: %{"__type" => %{"inputFields" => input_fields}}}} = resp + assert Enum.any?(input_fields, &(&1["name"] == "nonFieldCalc")) + end + + test "field?: false calculations are included in sort fields" do + resp = + """ + query { + __type(name: "PostSortField") { + enumValues { + name + } + } + } + """ + |> Absinthe.run(AshGraphql.Test.Schema) + + assert {:ok, %{data: %{"__type" => %{"enumValues" => sort_fields}}}} = resp + assert Enum.any?(sort_fields, &(&1["name"] == "NON_FIELD_CALC")) + end end diff --git a/test/read_test.exs b/test/read_test.exs index 3328425a..70967ad0 100644 --- a/test/read_test.exs +++ b/test/read_test.exs @@ -540,7 +540,41 @@ defmodule AshGraphql.ReadTest do result end - test "field?: false calculations can be queried via graphql" do + test "field?: false calculations are not exposed as response fields by default" do + resp = + """ + query { + __type(name: "Post") { + fields { + name + } + } + } + """ + |> Absinthe.run(AshGraphql.Test.Schema) + + assert {:ok, %{data: %{"__type" => %{"fields" => fields}}}} = resp + refute Enum.any?(fields, &(&1["name"] == "nonFieldCalc")) + end + + test "include_non_field_calculations exposes listed field?: false calculations" do + resp = + """ + query { + __type(name: "Post") { + fields { + name + } + } + } + """ + |> Absinthe.run(AshGraphql.Test.Schema) + + assert {:ok, %{data: %{"__type" => %{"fields" => fields}}}} = resp + assert Enum.any?(fields, &(&1["name"] == "includedNonFieldCalc")) + end + + test "included non-field calculations resolve from loaded calculations" do AshGraphql.Test.Post |> Ash.Changeset.for_create(:create, text: "bar", text1: "hello", published: true) |> Ash.create!() @@ -549,7 +583,7 @@ defmodule AshGraphql.ReadTest do """ query PostLibrary($published: Boolean) { postLibrary(published: $published) { - nonFieldCalc + includedNonFieldCalc } } """ @@ -563,7 +597,7 @@ defmodule AshGraphql.ReadTest do refute Map.has_key?(result, :errors) - assert %{data: %{"postLibrary" => [%{"nonFieldCalc" => "non_field: hello"}]}} = + assert %{data: %{"postLibrary" => [%{"includedNonFieldCalc" => "included_non_field: hello"}]}} = result end diff --git a/test/support/resources/post.ex b/test/support/resources/post.ex index 71b78544..d4d059c4 100644 --- a/test/support/resources/post.ex +++ b/test/support/resources/post.ex @@ -195,6 +195,7 @@ defmodule AshGraphql.Test.Post do ] field_names text_1_and_2: :text1_and2 + include_non_field_calculations([:included_non_field_calc]) keyset_field :keyset paginate_relationship_with unpaginated_comments: :none @@ -668,6 +669,11 @@ defmodule AshGraphql.Test.Post do public?(true) field?(false) end + + calculate :included_non_field_calc, :string, expr("included_non_field: " <> text1) do + public?(true) + field?(false) + end end aggregates do