baldwindavid
TokenOperator - Dependency-free helper most commonly used for making clean keyword APIs to Phoenix context functions
I just released the first version of TokenOperator - A dependency-free helper most commonly used for making clean keyword APIs to Phoenix context functions.
I would love feedback/questions on the pattern, the code, issues, better ways to accomplish, etc. This is my first Elixir package so there are probably issues. Thanks!
https://github.com/baldwindavid/token_operator
Here is a quick intro from the documentation:
One thing I’ve struggled with dealing with Phoenix contexts is knowing how to specify the queries to make from the controller. For example, say we want to see a list of blog posts. Sometimes we want that list paginated, sometimes only published, sometimes authors, sometimes with content, sometimes ordered by published date, etc.
We can always just create a bunch of functions on the context for every single variation. Here is an extremely contrived example for illustration:
Posts.list_published_posts_with_author_ordered_by_published_date_paginated(page: 7)
It would be nice to have a simple way to have an API with preset defaults similar to the following:
Posts.list_posts(
filter: [:featured, :published],
include: :author,
paginate: true,
page: 7,
order_by: :publish_date
)
TokenOperator makes it easy to develop a keyword-based API such as this, using the keywords that make sense for your application. The most obvious use case relates to operating on an Ecto query, but it can operate on any token and has no dependencies.
Most Liked
OvermindDL1
What this library does is near identical to what I manually do in most of my projects
It’s not hard at all to do manually, though this library does make the pattern more obvious.
baldwindavid
Glad to know this is similar to a pattern you might already be using. To your point, the entire library/helper is only about 20 LOC and can pretty easily be done inline. The value to me is definitely more about documenting a consistent pattern, a clean external API, and not recreating the wheel (even if easily recreated) in every context.
I suspect a lot of experienced Phoenix devs already have some sort of helper they are using that serves the same type of purpose.
baldwindavid
I’ve now moved one project fully to this alternate style and it feels pretty good thus far. It is certainly more verbose in the controller, but very easy to see what is going on.
In the controller…
featured_projects =
ProjectMgmt.list_projects([
&ProjectMgmt.order_projects_by_name/1,
&ProjectMgmt.preload_project_photos/1,
&ProjectMgmt.preload_project_categories/1,
&ProjectMgmt.filter_featured_projects/1,
&ProjectMgmt.filter_published_projects/1
])
As opposed to how it is using TokenOperator…
featured_projects =
ProjectMgmt.list_projects(include: [:photos, :categories], filter: [:featured, :published])
Clearly it is much cleaner in the controller with TokenOperator. Additionally, the concept of “defaults” is built in. Thus, I would have pre-configured list_projects to sort by name as an overridable default.
This alternative route loses the built in defaults, so order_projects_by_name is passed explicitly, but I don’t think that’s so bad. And there is no question of what order these queries will be run.
For more complex queries involving scoping based upon user permissions, I think this alternate method is easier to follow. Suppose I want to scope the rooms that a user can reserve. I was using the following with TokenOperator in the controller…
Inventory.list_rooms_for(location,
scope: {user, :reserve}
)
That eventually calls a scope function within my context, but there are some gymnastics involved where it isn’t immediately obvious how it gets there.
This alternate method is more obvious in the controller…
Calendar.list_rooms([
&Calendar.order_rooms_by_name/1,
&Calendar.scope_rooms(&1, user, :reserve))
])
In practice, I’m hiding that ugly bit of code to run those queries off in a utility:
defmodule Utilities.QueryRunner do
def run(query, additional_queries) when is_list(additional_queries) do
Enum.reduce(additional_queries, query, fn additional_query, query ->
additional_query.(query)
end)
end
def run(query, additional_query) do
run(query, [additional_query])
end
end
This is then referenced within the context:
alias Utilities.QueryRunner
def list_projects(queries \\ []) do
Project
|> QueryRunner.run(queries)
|> Repo.all()
end
As of now I’m leaning toward this simplified method, but both have their advantages.
Popular in Announcing
Other popular topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #websockets
- #supervisor
- #advent-of-code
- #elixirconf-us
- #distillery
- #processes
- #forms
- #api
- #metaprogramming
- #security
- #performance









