absinthe-graphql / absinthe

The GraphQL toolkit for Elixir
http://absinthe-graphql.org
Other
4.29k stars 529 forks source link

Representation of graphql schema models in Elixir #585

Closed tim2CF closed 6 years ago

tim2CF commented 6 years ago

Hello. I have a question about graphql schema code generation and about resolvers. Graphql schema is strict thing with fixed amount of specific fields of specific types. But why it is not expressed in Elixir? I think it would be reasonable to operate in resolvers not just with maps, but with auto-generated (from schema) Elixir structures and typespecs.

For example similar thing was implemented for Google Protobuf here https://github.com/coingaming/exprotobuf In this library every Protobuf message generates in compile-time relevant Elixir structure and typespec for it, because in .proto file is provided all information about fields and types.

The same information is provided in graphql schema. Structures/typespecs simplify development process a lot - if schema was changed (for example changed field name), but related resolver code was not changed - then compiler will say about it (with maps - will not).

So the question: why in absinthe resolvers data is just a map, but not structures with typespcs

benwilson512 commented 6 years ago

Hey @tim2CF great question. I'll start by saying this: You seem to be conflating GraphQL types with the data types returned by resolvers, when really there is no such association at all.

So for example lets consider the following GraphQL type:

query do
  field :current_user, :user do
    resolve &SomeModule.current_user/3
  end
end

object :user do
  field :name, :string
  field :age, :integer
end

It sounds like you're saying that you'd expect Absinthe to generate a user type, and that the &SomeModule.current_user/3 would be expected to return this type. What type would we generate though? The &SomeModule.current_user/3 function can return literally any elixir value.

Can you provide a more concrete proposal here?

tim2CF commented 6 years ago

Hello @benwilson512 ! Ok, let's consider we have this module (I just put query argument to your example)

defmodule Schema do

  query name: "RootQueryType" do
    field :user, :user do
      arg :id, non_null(:id)
      resolve &Resolver.user/2
    end
  end

  object :user do
    field :id, :integer
    field :name, :string
    field :age, :integer
  end

end

Then compiler will generate couple Elixir modules / structures / types from this code, something like

defmodule Schema.User do
  defstruct [
    :id,
    :name, 
    :age
  ]
  @type t :: %__MODULE__{
    id:   nil | integer,
    name: nil | String.t,
    age:  nil | integer
  }
end

defmodule Schema.RootQueryType.Field.User do
  defstruct [
    :id
  ]
  @type t :: %__MODULE__{
    id: integer
  }
end

So, function-resolver &Resolver.user/2 should have a spec

@spec user(Schema.RootQueryType.Field.User.t, Absinthe.Resolution.t) :: Schema.User.t

And should accept arguments and return values as typed Elixir structures, not maps. The same idea for mutations and subscriptions.

benwilson512 commented 6 years ago

This is definitely well beyond any functionality we plan to add to Absinthe. For one thing, how could it possibly know to generate structs? What if a user is a process? What if it's some other data type. There needn't be a 1:1 mapping of GraphQL types to internal schema data types.

If someone wants to create a secondary library that auto generates this stuff by introspecting an Absinthe schema they are welcome to, but this is not functionality we plan to add to Absinthe. Absinthe is flexible, the data you use to power resolvers can be literally anything, we can't limit it to structs.

tim2CF commented 6 years ago

For one thing, how could it possibly know to generate structs? What if a user is a process? What if it's some other data type.

But Absinthe schema provides full info about request and response types? field macro always accept type, so compiler always knows should it generate structure (if type is object, list of objects or other nested type that contains object) or not (if type is scalar)

I can not imagine example when object type can not be represented as Elixir structure

benwilson512 commented 6 years ago

I can not imagine example when object type can not be represented as Elixir structure

This is the fundamental disconnect I think: There is no contract between the return value of a resolution function and the response shape. Absinthe makes absolutely no restrictions about the Elixir data types you are allowed to use. While structs are common, it's also VERY common to use GraphQL field names that differ from the struct names. IE:

field :is_admin, :boolean, resolve: fn parent, _, _ -> {:ok, parent.admin?} end

What would Absinthe do in this case? Generating a struct type spec with is_admin would be wrong. Moreover structs are tied to modules, what module would it use?

tim2CF commented 6 years ago

Could you provide example, I think now I don't understand

Here

field :is_admin, :boolean

Type of this field is boolean, of course no reason to generate structure for standard types, in function specs standard boolean type can be used. I'm talking about 4 cases where structures and custom types can be useful

1) for queries 2) for mutations 3) for subscriptions 4) for objects (custom user types)

tim2CF commented 6 years ago

Ok, I think now I understand this - you mean middlewares

field :hello, :string do
  middleware MyApp.Web.Authentication
  resolve &get_the_string/2
  middleware HandleError, :foo
end

Example from documentation - typespec of resolver here related to middlewares, not to graphql schema

benwilson512 commented 6 years ago

I mean for a field within an object:

object :user do
  field :is_admin, :boolean, resolve: fn parent, _, _ -> {:ok, parent.admin?} end
end

Trying to generate a struct definition from this would result in an is_admin struct field, which would be wrong.

Setting aside the technical challenges, I think the approach is in general would undermine a core feature of using GraphQL: Your external schema and your internal data model can be different.

If this is something you really want to have happen you're welcome to write a third party library to do so. I'm nominally on vacation so I may not follow further responses for a while.

leostera commented 5 years ago

Has this been revisited somewhere? I can't seem to find any information on generating typespecs and not providing them means I'm bound to write them manually.