Absinthe: Allow partitioning schemas by concern

Created on 12 Jan 2017  路  5Comments  路  Source: absinthe-graphql/absinthe

I'd like to do something like:

defmodule App.API do

  import_schema App.API.Orders
import_schema App.API.Users

end

defmodule App.API.Orders do

  query do
    ...
  end

  mutation do
    ...
  end

end

Then I'd include my types, queries, mutations, resolvers, and everything relevant to a specific aspect of my API in a single file.

As per an earlier suggestion, I'm currently doing:

defmodule App.API do

  use Absinthe.Schema

  import_types App.API.Orders
  import_types App.API.Users

  query do
    import_fields :order_queries
    import_fields :user_queries
  end

end

defmodule App.API.Users do
  use Absinthe.Ecto, repo: App.Repo
  use Absinthe.Schema
  use Absinthe.Schema.Notation
  alias App.Repo
  alias App.Session
  alias App.User
  import App.API.Helpers

  import_types App.API.Orders

  object :user do
    ...
  end

  object :user_queries do
    ...
  end

end

Issues:

  • The proposed syntax is a bit ugly if I split my schema by concerns. I'd really like to just have query/mutation blocks in each subschema, then merge them in App.API..
  • The code as presented doesn't compile, because as soon as I run import_types App.API.Orders in App.API.Users to create an orders field containing all of a user's orders, I get errors about duplicated types. I get why that's happening, since I'm pulling in App.API.Orders both in App.API to merge queries and in App.API.Users to define the type on the field.

Maybe there's a reason that makes this a bad idea, but the documented style of lumping the entire API in a single file, then breaking off the resolvers, feels a bit strange to me. If I'm working on my Orders subsystem, it makes a lot more sense to just open orders.ex, where I have all my types/resolvers/queries/mutations. It also encourages code reuse, in that I can copy users.ex into another project where I want a similar user API, then just cut out the associations to models that don't apply. Otherwise I have to copy web/api.ex, carve it down, copy my resolvers, etc.

Thanks, I hope there's a way to manage this. In the meantime I guess I'll refactor my API modules to lump the schema into a single file and split out the resolvers. Either way, this is a fun API to work with once you get its various wrinkles. :)

Most helpful comment

@bruce This seems like an important feature because large apps could have schema.ex and types.ex files that get to be thousands and thousands of lines. I'm not an elixir expert, but maybe this could be done right now manually by using Elixir's metaprogramming features?

A folder structure could be:

  • types

    • post.ex

    • comment.ex

    • user.ex

  • mutations

    • post_mutations.ex

    • comment_mutations.ex

    • user_mutations.ex

  • queries

    • post_queries.ex

    • comment_queries.ex

    • user_queries.ex

  • connections

    • post_connections.ex

    • comment_connections.ex

    • user_connections.ex

All 5 comments

Keep in mind that you're building _a_ schema, not multiple schemas, and at the end of that day that schema must have _one_ root type for each operation type (eg, query, mutation, subscription -- with query being mandatory). You can split concerns, but you can't redefine those root types in multiple modules, especially because all of this is occurring at compile time -- and avoiding accidental redefinitions (and not auto-merging fields) is an intentional design choice.

I'll keep this in mind while we build out v1.3, however, which changes things so that the schema definition macros actually work by modifying a representation, which is then "compiled down" into functions at the end. That may make more flexible patterns possible -- but we'll continue to err on the side of explicit declarations.

@bruce This seems like an important feature because large apps could have schema.ex and types.ex files that get to be thousands and thousands of lines. I'm not an elixir expert, but maybe this could be done right now manually by using Elixir's metaprogramming features?

A folder structure could be:

  • types

    • post.ex

    • comment.ex

    • user.ex

  • mutations

    • post_mutations.ex

    • comment_mutations.ex

    • user_mutations.ex

  • queries

    • post_queries.ex

    • comment_queries.ex

    • user_queries.ex

  • connections

    • post_connections.ex

    • comment_connections.ex

    • user_connections.ex

Hey @Un3qual, the tooling has developed a bit since this issue was created. See https://hexdocs.pm/absinthe/1.4.0-beta.3/Absinthe.Schema.Notation.html#import_fields/2

@Un3qual As @benwilson512 points out, nothing's stopping anyone from using import_types and import_fields these days to define the structure you're indicating (but without allowing the root types to be defined outside the schema module), eg:

defmodule MyApp.Web.Schema do
  use Absinthe.Schema

  import_types MyApp.Web.Schema.Mutations.PostMutations
  # others...

  mutation do
    import_fields :post_mutations
    # others...
  end

  query do
    # others...
  end

end

We could certainly make things easier (eg, support syntax like import_types MyApp.Web.Schema.Mutations.{PostMutations, CommentMutations}), but this approach is currently workable with the bonus of not being very "magical" (a good thing, generally speaking). My recommendation is generally to treat your main schema module like a manifest, pulling everything in.

@bruce @benwilson512 Thanks! Works like a dream 馃槃

Was this page helpful?
0 / 5 - 0 ratings