Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introspection query fails when using a prototype schema that defines types for use by directive arguments #1279

Closed
dhrdlicka opened this issue Nov 8, 2023 · 6 comments · Fixed by #1334

Comments

@dhrdlicka
Copy link

dhrdlicka commented Nov 8, 2023

If submitting a bug, please provide the following:

Environment

  • Elixir version (elixir -v): 1.14.4
  • Absinthe version (mix deps | grep absinthe): 1.7.5
  • Client Framework and version (Relay, Apollo, etc): N/A

Expected behavior

When using a prototype schema that defines directives as well as types used for the directives' arguments, the introspection query should succeed and return directives used by the schema as well as the argument types.

Actual behavior

The introspection query fails:

iex(1)> Absinthe.Schema.introspect(MyAppWeb.Schema)
{:ok,
 %{
   data: %{"__schema" => nil},
   errors: [
     %{
       locations: [%{column: 3, line: 64}],
       message: "Cannot return null for non-nullable field",
       path: ["__schema", "directives", 2, "args", 0, "type"]
     }
   ]
 }}

Poking around the schema (not the prototype schema) using Absinthe.Schema.lookup_* functions reveals that the directives resolve properly but looking up the custom argument types returns nil:

iex output
iex(1)> Absinthe.Schema.lookup_directive(MyAppWeb.Schema, "feature")
%Absinthe.Type.Directive{
  name: "feature",
  description: nil,
  identifier: :feature,
  args: %{
    complex: %Absinthe.Type.Argument{
      identifier: :complex,
      name: "complex",
      description: nil,
      type: :complex,
      deprecation: nil,
      default_value: nil,
      definition: nil,
      __reference__: nil
    },
    name: %Absinthe.Type.Argument{
      identifier: :name,
      name: "name",
      description: nil,
      type: %Absinthe.Type.NonNull{of_type: :string},
      deprecation: nil,
      default_value: nil,
      definition: nil,
      __reference__: nil
    },
    number: %Absinthe.Type.Argument{
      identifier: :number,
      name: "number",
      description: nil,
      type: :integer,
      deprecation: nil,
      default_value: nil,
      definition: nil,
      __reference__: nil
    }
  },
  locations: [:argument_definition, :enum, :enum_value, :field_definition,
  :input_field_definition, :input_object, :interface, :object, :scalar,
  :schema, :union],
  expand: {:ref, MyAppWeb.SchemaPrototype,
  {Absinthe.Blueprint.Schema.DirectiveDefinition, :feature}},
  definition: MyAppWeb.SchemaPrototype,
  repeatable: true,
  __private__: [],
  __reference__: %{
    location: %{
      file: "/home/david/Documents/com5737-repro/repro/lib/my_app_web/schema_prototype.ex",
      line: 12
    },
    module: MyAppWeb.SchemaPrototype
  }
}
iex(2)> Absinthe.Schema.lookup_type(MyAppWeb.Schema, "Complex")     
nil
iex(3)> Absinthe.Schema.lookup_type(MyAppWeb.SchemaPrototype, "Complex")
%Absinthe.Type.InputObject{
  name: "Complex",
  description: nil,
  fields: %{
    str: %Absinthe.Type.Field{
      identifier: :str,
      name: "str",
      description: nil,
      type: :string,
      deprecation: nil,
      args: %{},
      config: nil,
      triggers: [],
      middleware: [],
      complexity: nil,
      default_value: nil,
      __private__: [],
      definition: MyAppWeb.SchemaPrototype,
      __reference__: %{
        location: %{
          file: "/home/david/Documents/com5737-repro/repro/lib/my_app_web/schema_prototype.ex",
          line: 5
        },
        module: MyAppWeb.SchemaPrototype
      }
    }
  },
  identifier: :complex,
  __private__: [__absinthe_referenced__: true],
  definition: MyAppWeb.SchemaPrototype,
  __reference__: %{
    location: %{
      file: "/home/david/Documents/com5737-repro/repro/lib/my_app_web/schema_prototype.ex",
      line: 4
    },
    module: MyAppWeb.SchemaPrototype
  }
}

Relevant Schema/Middleware Code

I really just copied the schemas from type_system_directive_test.exs.

Prototype schema

Code
defmodule MyAppWeb.SchemaPrototype do
  use Absinthe.Schema.Prototype

  input_object :complex do
    field :str, :string
  end

  directive :external do
    on [:field_definition]
  end

  directive :feature do
    arg :name, non_null(:string)
    arg :number, :integer
    arg :complex, :complex

    repeatable true

    on [
      :schema,
      :scalar,
      :object,
      :field_definition,
      :argument_definition,
      :interface,
      :union,
      :enum,
      :enum_value,
      :input_object,
      :input_field_definition
    ]
  end
end

Schema

Code
defmodule MyAppWeb.Schema do
  use Absinthe.Schema

  @prototype_schema MyAppWeb.SchemaPrototype

  schema do
    directive :feature, name: ":schema"
    field :query, :query
  end

  query do
    field :post, :post do
      directive :feature, name: ":field_definition"
    end

    field :sweet, :sweet_scalar
    field :which, :category
    field :pet, :dog

    field :search, :search_result do
      arg :filter, :search_filter, directives: [{:feature, name: ":argument_definition"}]
      directive :feature, name: ":argument_definition"
    end
  end

  object :post do
    directive :feature, name: ":object", number: 3

    field :name, :string do
      deprecate "Bye"
    end
  end

  scalar :sweet_scalar do
    directive :feature, name: ":scalar"
    parse &Function.identity/1
    serialize &Function.identity/1
  end

  enum :category do
    directive :feature, name: ":enum"
    value :this
    value :that, directives: [feature: [name: ":enum_value"]]
    value :the_other, directives: [deprecated: [reason: "It's old"]]
  end

  interface :animal do
    directive :feature, name: ":interface"

    field :leg_count, non_null(:integer) do
      directive :feature,
        name: """
        Multiline here?
        Second line
        """
    end
  end

  object :dog do
    is_type_of fn _ -> true end
    interface :animal
    field :leg_count, non_null(:integer)
    field :name, non_null(:string), directives: [:external]
  end

  input_object :search_filter do
    directive :feature, name: ":input_object"

    field :query, :string, default_value: "default" do
      directive :feature, name: ":input_field_definition"
    end
  end

  union :search_result do
    directive :feature, name: ":union"
    types [:dog, :post]

    resolve_type fn %{type: type}, _ -> type end
  end
end
@cschiewek
Copy link

We're also experiencing this issue. Could this be related to custom schema directives not showing in in SDL as well?

@benwilson512 We're happy to spend some time working on this, any chance you could provide some direction?

@jeffutter
Copy link
Contributor

I'm running into this as well. I've pared down the above example code a bit into a unit test that can be dropped into test/absinthe/introspection_test.exs:

  describe "introspection of complex directives" do
    defmodule ComplexDirectiveSchema do
      use Absinthe.Schema
      use Absinthe.Fixture

      defmodule ComplexDirectivePrototype do
        use Absinthe.Schema.Prototype

        input_object :complex do
          field :str, :string
        end

        directive :complex_directive do
          arg :complex, :complex

          on [:field]
        end
      end

      @prototype_schema ComplexDirectivePrototype

      query do
        field :foo,
          type: :string,
          args: [],
          resolve: fn _, _ -> {:ok, "foo"} end
      end
    end

    test "renders type for complex directives" do
      result =
        """
        query IntrospectionQuery {
          __schema {
            directives {
              name
              args {
                name
                description
                type {
                  kind
                  name
                }
                defaultValue
              }
            }
          }
        }
        """
        |> run(ComplexDirectiveSchema)

      assert {:ok,
              %{
                data: %{
                  "__schema" => %{
                    "directives" => [
                      %{
                        "name" => "complexDirective",
                        "args" => [%{"type" => %{"kind" => "OBJECT", "name" => "complex"}}]
                      }
                    ]
                  }
                }
              }} = result
    end
  end

I'm not 100% sure on the return type for result it should probably have some details in it about the complex type but it shows the error either way:

{
  :ok,
  %{
    data: %{"__schema" => nil},
    errors: [%{locations: [%{column: 9, line: 8}], message: "Cannot return null for non-nullable field", path: ["__schema", "directives", 0, "args", 0, "type"]}]
  }
}

@kzlsakal
Copy link
Contributor

@benwilson512 sorry to bother you but it would be great if you had any pointers for this issue. We currently cannot get introspection at all if the schema uses the @link directive.

@antonioparisi
Copy link

any update on this?

jeffutter added a commit to jeffutter/absinthe that referenced this issue Aug 2, 2024
This fixes an issue when you have types declared in a Prototype (such as
input_object input types for a directive). Previously type resolution
would look on the Schema, but not the Prototype for the type
definitions.

You _could_ workaround this by duplicating your type definition into
both the Schema and the Prototype. This makes me somewhat curious if
there is a better way to fix this, in that the code that was trying to
resolve the type here shouldn't be resolving it on the Schema, but
rather on the Prototype 🤷. This seems to work though.

Fixes absinthe-graphql#1279
@jeffutter
Copy link
Contributor

I may have a fix for this.

It fixes the test I included above. I haven’t tested it out with absinthe_federation and @link yet though.

main...jeffutter:absinthe:recurse-prototype-type-resolution

I’ll give it a more thorough test tomorrow and open a PR if it seems to work.

@jeffutter
Copy link
Contributor

Confirmed that code allows us to execute both the federation sdl query and the traditional introspection query that utilizes @link.

jeffutter added a commit to scoremedia/absinthe that referenced this issue Aug 2, 2024
This fixes an issue when you have types declared in a Prototype (such as
input_object input types for a directive). Previously type resolution
would look on the Schema, but not the Prototype for the type
definitions.

You _could_ workaround this by duplicating your type definition into
both the Schema and the Prototype. This makes me somewhat curious if
there is a better way to fix this, in that the code that was trying to
resolve the type here shouldn't be resolving it on the Schema, but
rather on the Prototype 🤷. This seems to work though.

Fixes absinthe-graphql#1279
jeffutter added a commit to jeffutter/absinthe that referenced this issue Aug 2, 2024
This fixes an issue when you have types declared in a Prototype (such as
input_object input types for a directive). Previously type resolution
would look on the Schema, but not the Prototype for the type
definitions.

You _could_ workaround this by duplicating your type definition into
both the Schema and the Prototype. This makes me somewhat curious if
there is a better way to fix this, in that the code that was trying to
resolve the type here shouldn't be resolving it on the Schema, but
rather on the Prototype 🤷. This seems to work though.

Fixes absinthe-graphql#1279
jeffutter added a commit to scoremedia/absinthe that referenced this issue Aug 2, 2024
This fixes an issue when you have types declared in a Prototype (such as
input_object input types for a directive). Previously type resolution
would look on the Schema, but not the Prototype for the type
definitions.

You _could_ workaround this by duplicating your type definition into
both the Schema and the Prototype. This makes me somewhat curious if
there is a better way to fix this, in that the code that was trying to
resolve the type here shouldn't be resolving it on the Schema, but
rather on the Prototype 🤷. This seems to work though.

Fixes absinthe-graphql#1279
jeffutter added a commit to scoremedia/absinthe that referenced this issue Aug 5, 2024
This fixes an issue when you have types declared in a Prototype (such as
input_object input types for a directive). Previously type resolution
would look on the Schema, but not the Prototype for the type
definitions.

You _could_ workaround this by duplicating your type definition into
both the Schema and the Prototype. This makes me somewhat curious if
there is a better way to fix this, in that the code that was trying to
resolve the type here shouldn't be resolving it on the Schema, but
rather on the Prototype 🤷. This seems to work though.

Fixes absinthe-graphql#1279
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants