What's the best way to have composed queries in the context layer, similar to ActiveRecord scopes?

I am making a search/filter feature for my app. I have a schema called Initiatives that I want to filter. I want to filter Initiative by organization and then a search query on the Initiative. What’s the best practice to do this from the context layer? Where should I define functions that only return queries?

def list_initiatives do
    Repo.all(Initiative)
end

All of the generated functions in the context layer call directly into the repo, so by the time the function returns, I can’t modify the query without executing another one.

I’d like to do something like below, but the standard seems to be not to expose ecto queries directly to the web layer. It would be easy if I could define a few functions that return a query and accept a query and then execute it with Repo.all in the liveview. How do I do this idiomatically?

Initiative
|> by_organization(organization)
|> by_search_keyword()
|> Repo.all()

Is it okay to define these query-returning functions in the schema’s module?

2 Likes

Probably dynamic queries is what you are looking for.

2 Likes

I would most likely do this:

Context.list_initiatives_by_organization(organization, search_parameters)

And have all of the logic inside the context. This allows the LiveView to focus on UI concerns and makes it easier to test the different search keywords.

In any case, you may still want to compose. You can still have your original code in the context:

Initiative
|> by_organization(organization)
|> by_search_keyword()
|> Repo.all()

And you would implement by_organization, for example, like this:

defp by_organization(query, organization) do
  from i in query, where: ^organization.id == i.organization_id
end

Or using the pipe syntax:

defp by_organization(query, organization) do
  where([i], i.organization_id == ^organization.id)
end

Notice the schema itself (Initiative) is a query (which is why you can pass it to Repo.all).

6 Likes

Ah I see. I went back through some code from a book I read, and they are keeping queries in a module “after” the schema module. What do you think of this approach?

It would be something like “lib/app/initiatives/initiative/query.ex” and the contents would have a bunch of queries:

defmodule App.Initiatives.Initiative.Query do
  def base, do: App.Initiatives.Initiative

  def by_organization(query \\ base(), organization) do
    from i in query, where: ^organization.id == i.organization_id
  end
end

It really doesn’t matter as far as running the code goes. Structure it in the way that you and your team find intuitive. These things are mostly preferences and rarely carry any real weight.