How to architecture umbrella project around Absinthe and its ecosystem?

Hi, I have an umbrella project with 4 different apps:

  • Messenger: Handles email, push notifications, etc
  • Data: Ecto Repo, schemas and queries
  • Core: exposes services, handles authorization, depends on Data and Messenger apps.
  • API: Phoenix app, already handles some REST endpoints and authentication. Depends con Core app.

I’m trying to add Absinthe to the project, I’m currently reading the Craft GraphQL APIs in Elixir with Absinthe book by @benwilson512 and looked at some projects with an implementation. However I haven’t find an example of how to organize code under an umbrella project.

In this particular project a request will be handled something like this:

API.Router 
|> API.Plug.Authenticate 
|> API.MyModelController 
|> Core.MyModelService (Authorization, limiting, notifications) 
|> Data.MyModelQueries
|> Messenger.send_something_if_needed

With this model, It’s pretty obvious where to put everything, each app has well defined boundaries, but adding Absinthe and it’s ecosystem to the mix seems a little confusing.

Firstly, where should I add each dependency?
absinthe and absinthe_plug belong to the API app, but what about dataloader or absinthe_ecto? should they go inside the Data app?
The problem is that these projects all seem very tied to each other, and looks counter intuitive to spread them across different apps.

The problem arises if I add them all to the API app since I don’t want (and is a bad practice) to alias/call Data or Ecto related modules from it, since its not a direct dependency.

Next, where should resolvers live? Is it a good idea to put them in the API app just like controllers and call the same services REST endpoints use from Core app?
Or should they live inside the Core app itself? (I’ve seen projects where resolvers are tied to contexts and not to the web side)

It’s the first time I’m working with Graphql so maybe I’m missing something?
Using the example above, how a Graphql request should look like starting from the router?

Any guidance in which direction to go will be appreciated.

2 Likes

You should put all Absinthe dependencies into the Web part.

The Ecto part does not need to know about Absinthe at all.

I would put resolvers in the web part too, this is where GraphQL is.

But as You have your own structure, YMMV.

That’s exactly what I was thinking, neither the Data or the Core app should be aware of what web app is doing and just respond requests to the services exposed.

Thanks for the input!

More or less… You will go from

API.Router 
|> API.Plug.Authenticate 
|> API.MyModelController 
|> Core.MyModelService (Authorization, limiting, notifications) 
|> Data.MyModelQueries
|> Messenger.send_something_if_needed

to

API.Router 
|> Absinthe
|> Messenger.send_something_if_needed

BTW, if You follow the book, and even if the example is not an umbrella application, You can clearly see that Absinthe is in the Web part, and uses resolvers calling into the Ecto part.

Yes, I can see that for absinthe, but for example, if intended to use with Ecto, dataloader's documentation starts by adding a source:

source = Dataloader.Ecto.new(MyApp.Repo)

So If I use it I’ll probably need to break the boundary of the apps or place it on the Core app which has direct dependency on Data.Repo module.

Still have to make a choice, and unfortunately none of them convinces me.

Probably I’ll just compose more complex Ecto queries to have the same functionality.

1 Like

I think you’ll have to add dataloader into your core app (which it seems you have mostly figured out). Although I think it’ll be a little hard to maintain a nice clean separation, partially because it is a bit difficult/awkward to use dataloader from something other than Absinthe.

Why not adding Dataloader to Data app and expose it to API through Core? Maybe it’s a little overkill but you can put Ecto implementation details inside query function and I think that should belong to Data app.

Smth like:

defmodule Data.MyModule.Dataloader do
  def data do
    Dataloader.Ecto.new(DbServer.Repo, query: &query/2)
  end

  def query(queryable, _params) do
    queryable
  end
end

defmodule Core.MyModule do
  def dataloader_source do
    Data.MyModule.Dataloader.data()
  end
end

defmodule API.Graphql.Schema do
  def context(ctx) do
    loader =
      Dataloader.new()
      |> Dataloader.add_source(:my_module, Core.MyModule.dataloader_source())

    Map.put(ctx, :loader, loader)
  end
end

And query function with ecto:

def query(Organization, %{current_user: user}) do
  from o in Organization,
    join: m in assoc(o, :memberships),
    where: m.user_id == ^user.id
end
def query(queryable, _) do
  queryable
end

Yes, that’s what I’m worried about. As I said all these projects look very tied to each other, making it difficult to separate boundaries when working with an umbrella structure.

Looks you’re calling Dataloader from the Data and the API apps, should it be added as a dependency in both apps or just call it even if it’s not a direct dependency of API?

This is what I did in my project, added dataloader as dependency of both apps (API + Data). Totally agree that it’s hard to separate it nicely within umbrella project.

Maybe it would be a good idea to separate Dataloader.Ecto into its own package?

1 Like

I had the same problem, and was very annoyed by the tied coupling that ensued with using dataloader on an umbrella app containing an Absinthe API app amongst other domain-based apps. I ended up having change my resolution logic with Absinthe.Middleware.Async, and it worked OK.

I also tried making dataloader work from within the domain apps, but using dataloader basically meant I was giving up my internal APIs (I may haven’t tried hard enough here though, take that with a grain of salt) in favor of query generation.

note that this was done on a side project to try things out, I haven’t really pushed the concept further.

yeah this is the sore spot that I was alluding to in my earlier post, and is something that I’ve personally experienced as well. @jfrolich was investigating an alternative/enhanced api that would make using dataloader between domain apps much more pleasant/useful, but I’m not sure if he’s still continuing that investigation: WIP: Deferrable dataloader by jfrolich · Pull Request #21 · absinthe-graphql/dataloader · GitHub

Also, here’s another related thread (that I posted a while back): A comprehensive approach to data loading and permissions

I think that in that case the best thing to do is to define a custom source for your domain applications. The Dataloader package actually allows for this: https://hexdocs.pm/dataloader/Dataloader.Source.html#content

mental note: I should actually look into that.

I’m running into some of the same issues here - if anyone has any tips I’d really appreciate it!