TokenOperator - Dependency-free helper most commonly used for making clean keyword APIs to Phoenix context functions

This plugin is working just fine for me, but I still can’t kick the nagging feeling that there might be a simpler, better way that is more maintainable and obvious.

One thing that can be done is to expose some of these queries in the context to the controller like @josevalim mentions at Phoenix Contexts - is it common to preload all resources on the context methods with whatever everything needs access to?

For example, say you have users with profiles that you want to “preload”, but not “join”. This is going to result in two queries no matter what.

In the controller, you can do something like…

user = Accounts.get_user!(id)
user_with_profile = user |> Accounts.preload_profile()

However, if you want to join or run a query prior to get_user! having hit the database, I don’t think that is going to work.

Maybe something like this could provide for flexible querying, but with more transparency and easier maintainability than the pattern that this plugin results in…

def get_user!(id, queries \\ []) do
  from(User)
  |> (fn query -> Enum.reduce(queries, query, & &1.(&2)) end).()
  |> Repo.get!(id)
end

def join_profile(query) do
  from u in query, join: p in (u, :profile), preload: [profile: p]
end

Now in the controller, you are free to reference queries exposed from the context, but without being coupled to ecto.

user = Accounts.get_user!(id, [&Accounts.join_profile/1])

Filtering and passing additional arguments is also fine like in this contrived example in a Blog context:

def list_posts(queries \\ []) do
  from(Post)
  |> (fn query -> Enum.reduce(queries, query, & &1.(&2)) end).()
  |> Repo.get!(id)
end

def published_posts(query) do
  from p in query, where: p.is_published
end

def order_posts_by(query, attr_name) do
  from query, order_by: ^attr_name
end

Those functions can be referenced in the controller via list:

posts = Blog.list_posts([
  &Blog.published_posts/1,
  &Blog.order_posts_by(&1, :title)
])

The thing I like about this is that it is easy to see and track down the context functions that are being used in the controller, which might make maintainability easier.

I don’t like how much larger the context api becomes and it doesn’t look as clean in the controller, but I’m not sure I’d prioritize that over the transparency and easier reasoning about direct references to queries.

I guess I think of TokenOperator providing the ability to setup an API that is similar to the sort of thing you would use when hitting an external API. However, maybe that is needlessly abstracting away from just exposing a few more queries from the context and referencing them directly in the controller.

@OvermindDL1 - I’d love to hear your thoughts if any of this strikes you as a useful (or abhorrent) pattern. Perhaps this is just a nuanced enough thing that there is no right answer though.

1 Like