Ninigi / ecto_dripper

Simple way to create composable ecto queries

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

EctoDripper

Hex.pm Build Status

Provides an easy way to create composable ecto queries following a convention of query_x(queryable, %{x: "asdf"}), or query_all(queryable, %{x: "asdf"}).

Installation

The package can be installed by adding ecto_dripper to your list of dependencies in mix.exs:

def deps do
  [
    {:ecto_dripper, "~> 0.1.0"}
  ]
end

Why not just write my queries in the already extremely readable ecto DSL??

Excellent question and I am glad you are not a mindless zombie 👌

If you don't know how to write queries in ecto, then this lib doesn't do anything for you but hide a few basic queries away. Don't use EctoDripper if you are not at least a little familiar with ecto queries yet, at some point you will have to use it anyways 🔥

I wrote this lib as part of another project when I realized I am following the same pattern over and over again, and that's all this lib does for you: Streamline some repetetive tasks and unclutter your query modules.

Composable Queries

Composable in this case means "pipable", as in create a bunch of queries you can pipe together, or use the convenience function query_all/2 to create queries from arguments.

Because of elixirs awesome pattern matching, we can just blindly pipe query functions together and only apply them if a certain key is in the arguments.

args = %{this_arg: "asdf"}
args2 = %{that_arg: "asdf"}

# Does only query for :this_arg
MyApp.MySchema
|> MyApp.MyQuery.query_all(args)

# Does only query for :that_arg
MyApp.MySchema
|> MyApp.MyQuery.query_all(args2)

# Or use a specific query
MyApp.MySchema
|> MyApp.MyQuery.query_this_arg(args)

Basic Usage

defmodule MyApp.SomeQuery do
  use EctoDripper,
    composable_queries: [
      [:status, :==, :status],
      [:max_height, :>, :height],
      [:status_down, :status_down]
    ],
    standalone_queries: [
      [:small_with_status_up]
    ]

  def status_down(query, args)
  def status_down(query, %{status_down: true}) do
    from(
      i in query,
      where: i.status == ^"down"
    )
  end
  def status_down(query, %{status_down: _}) do
    from(
      i in query,
      where: i.status != ^"down"
    )
  end

  def small_with_status_up(query, _args) do
    from(
      i in query,
      where: i.status == ^"up", i.height <= 10
    )
  end
end

MyThing
|> MyApp.SomeQuery.query_all(%{status: "somewhere", max_height: 30})
# #Ecto.Query<from i in MyThing, where: i.status == ^"somewhere", i.height > ^30>

# and use it with your Repo
MyThing
|> MyApp.SomeQuery.query_all(%{status: "up", max_height: 30})
|> Repo.all()
# [%MyThing{}, ..]

What's going on here? I'd like to know how this works before __using__

# What my modules usually looks like without EctoDripper
defmodule MyApp.MyQuery do
  def query_all(query, args) do
    query
    |> query_status(args)
    |> query_name_or_identifier(args)
  end

  def query_status(query, args)
  def query_status(query, %{status: status}) do
    # Create query when status key is in args
    from(
      thing in query,
      where: thing.status == ^status
    )
  end
  def query_status(query, _args) do
    # Let the query "fall through" if no status key in args
    query
  end

  def query_name_or_identifier(query, args)
  def query_name_or_identifier(query, %{query_name_or_identifier: query_name_or_identifier}) do
    # Create query when query_name_or_identifier key is in args
    from(
      thing in query,
      where: thing.name == ^query_name_or_identifier or thing.identifier == ^query_name_or_identifier
    )
  end
  def query_name_or_identifier(query, _args) do
    # Let the query "fall through" if no query_name_or_identifier key in args
    query
  end
end

# When using EctoDripper
defmodule MyApp.MyQuery do
  use EctoDripper,
    composable_queries:[
      [:status, :==],
      [:name_or_identifier, :do_query_name_or_identifier]
    ]

  def do_query_name_or_identifier(query, args) do
    from(
      thing in query,
      where: thing.name == ^query_name_or_identifier or thing.identifier == ^query_name_or_identifier
    )
  end
end

Slightly more advanced usage - aka "I am lazy, give me more convenience"

There are 2 different options, composable_queries and standalone_queries, both take a list of lists to create some basic functions for you.

A function option can consist of 2 or 3 keywords. When read from left to right, they describe the query function. For example with:

use EctoDripper,
  composable_queries: [
    [:status, :==]
  ]

[:status, :==] will create a query, comparing the status field of your queryable with the value for the status key in the args:

def query_status(query, %{status: _} = args) do
  for(
    thing in query,
    where: thing.status == ^args.status
  )
end

If you want your query argument different from the field name, you can do so by adding a third field to the option: [:my_status_thing, :==, :status]

def query_status(query, %{my_status_thing: _} = args) do
  for(
    thing in query,
    where: thing.status == ^args.my_status_thing
  )
end

[:name_or_identifier, :my_handler] calls your custom handler function when passed name_or_identifier in args.

def name_or_identifier(query, %{name_or_identifier: _} = args) do
  MyModule.my_handler(query, args)
end

Read more

A quick tutorial for usage in phoenix

Repo.insert(%Contribution{code: "def awesome"})

Contributions are highly welcome

  1. Fork it
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create new Pull Request

About

Simple way to create composable ecto queries

License:MIT License


Languages

Language:Elixir 100.0%