Sharing schema definitions between Ecto and Absinthe

Hi,

I have a Phoenix app, and there is a lot of duplication between Ecto schemas and GraphQL type definitions. Does anyone have the same problem and a solution to suggest ?

Thanks

1 Like

I’m looking too for a solution for this

I have tried to add procedurally fields to an Absinthe object, but it does not compile:

 object :company do
   field :id, :id
   
   for {field_atom, field_type} <- [{:name, :string}] do
     field field_atom, field_type
   end
 end

The error is
** (ArgumentError) argument error
:erlang.atom_to_binary({:field_atom, [line: 274], nil}, :utf8)
lib/absinthe/schema/notation.ex:1588: Absinthe.Schema.Notation.default_name/3

Using a Macro seems to work:

defmodule SchemaMacros do

  defmacro build_fields(fields) do

    for {f, t} <- fields do
      quote do
        field unquote(f), unquote(t)
      end
    end
  end

end

then in the schema:

object :company do
    field :id, :id
  
    build_fields([{:name, :string}])
end

Right but there’s no difference between explicit writing of object and call a function which has args defined ( you will get same number of lines in both cases )

I’d say this is a good thing because you wouldn’t want to automatically expose every Ecto (db!) field in your GraphQL endpoint.

Like it is good you need to be explicit about which fields you want the consumer to be able to access.

3 Likes

The idea is to get the args from somewhere else. A place defining the fields concerning an entity, and whether or not they have to be visible in GraphQL, in Ecto, …

Imagine something like this in an entity called Company:

def fields, do: [
  [{:name, :string, :visible_in_graphql, :validate_required_in_ecto},
   {:archived, :boolean, :non_visible_in_graphql, :non_validate_required_in_ecto}
]

So you would keep this fields definition up to date, and it would generate ecto schemas and graphql objects “automatically”.

My problem with this for the moment is that

object :company do
    field :id, :id
    build_fields([{:name, :string, nil, nil}])
end

compiles, but

object :company do
    field :id, :id
    build_fields(Company.fields())
end

does not. Any idea why ?

(Here is the macro)

defmacro build_fields(fields) do
    for {f, t, _, _} <- fields do
      quote do
        field unquote(f), unquote(t)
      end
    end
end
2 Likes

Macros work on the abstract syntax tree (AST) and not the result of executed code. Hardcoded values often represent itself in AST or are otherwise convertable to itself, which is why for example a macro can iterate over a list of atom values even if it’s AST, while Company.field() is just this in AST: {{:., [], [{:__aliases__, [alias: false], [:Company]}, :field]}, [], []}. There’s not sight of any useful information to generate a schema by. You could use a macro to return the list of fields as AST, which can then be used by build_fields and in the end by the field macro of absinth/ecto. But really as always with metaprogramming: use it in sane doses. It’s only getting more and more complex.

4 Likes

Writing documentation inline with your object definitions is a killer feature of GraphQL. You lose that ability when you auto generate the fields from a list like this.

You may have to write the schema and object out with a hefty bit of duplication, but there is good reason to do so.

This description of the field could be one of the data stored centrally though ?
Imagine this:

def fields, do: [
  [{:name, :string, :visible_in_graphql, :validate_required_in_ecto, "My cool description for Graphql"},
   {:archived, :boolean, :non_visible_in_graphql, :non_validate_required_in_ecto, nil}
]
1 Like

Hey everyone!

At this point in time dynamically generating Absinthe types / fields requires a fair bit of macro work, but this will be changing soon! In Absinthe 1.5 the macros will just generate ordinary elixir data structures, and it’s that data that will generate the schema. If you want to make schemas dynamically, you can just build those datastructures yourself and ditch the macros completely.

As others have pointed out though, you may want to avoid tightly coupling your ecto and Absinthe schemas. Your ecto schemas and database tables should be organized to help and facilitate your application logic, whereas your absinthe schema should be organized to facilitate client logic, and those aren’t always the same things.

8 Likes

Hi,
@benwilson512, your book is awesome ! I ended up doing a macro like @LostKobrakai suggested. It works but is is indeed a bit unsatisfactory. I’m looking forward to the 1.5 release to make this cleaner.

My ecto and graphql schemas are not identical. But there is lot of redundancy I want to avoid (plus I have a third way to provide access to the data, with some kind of schema very similar to the two others. In the end it meant having all definitions in three different schemas, very prone to bugs and painful to manage).

That makes sense. We’ll provide docs illustrating what to do.

2 Likes

By the way this is what I did (not sure if it’s right, if anything is), but it works in my case.

In my GraphQL schema:

  object :company do
    field :id, :id
    build_fields(Company.fields())
  end

In a macro module

  defmacro build_fields(fields_ast) do
    {fields, _} = Code.eval_quoted(fields_ast)
    for {f, t, _, _} <- fields do
      quote do
        field unquote(f), unquote(t)
      end
    end
end
2 Likes

Any update and example how this is implemented in the 1.5 beta releases?

Thanks.

Maybe a bit late, but take a look at dilute

2 Likes