Packages

EctoConditionals implements a flexibly functional find_or_create and upsert behavior for Ecto models.

Current section

Files

Jump to
ecto_conditionals lib ecto_conditionals.ex
Raw

lib/ecto_conditionals.ex

defmodule EctoConditionals do
@moduledoc """
Provides Ecto conditional helper functions for find_or_create-ing or upsert-ing records
## Basic Usage
First specify the repo you're going to use:
```elixir
use EctoConditionals, repo: MyApp.Repo
```
Then, pipe or pass the record struct to `find_or_create/1` or `upsert/1`.
These functions assume finding by id to determine whether to create or upsert.
If an id field is present in the record struct, the id field will be used
as the unique selector to determine whether to find, insert, or update.
If not present, these functions will insert a new record.
```elixir
%User{id: 1, name: "Flamel"} |> find_or_create
#=> {:ok, %User{id: 1, name: "Flamel"}}
%User{name: "Dumbledore"} |> upsert
#=> {:ok, %User{id: 2, name: "Dumbledore"}}
```
You can also specify selectors by instead using `find_or_create_by/2` or `upsert_by/2`
and passing a selector or list of selectors as the second argument.
```elixir
%User{name: "Slughorn"} |> find_or_create_by(:name)
#=> {:ok, %User{id: 3, name: "Slughorn"}}
%User{first_name: "Harry", last_name: "Potter"} |> upsert_by(:last_name)
#=> {:ok, %User{id: 4, first_name: "Harry", last_name: "Potter"}}
```
## Implementation Note
`find_or_create_by/2` is a thin wrapper piping through `find_by/2` and then `or_create/1`
`upsert_by/2` is a thin wrapper piping through `find_by/2` and then `update_or_insert/1`.
`find_or_create/1` is a thin wrapper piping through `find/1` and then `or_create/1`
and `upsert/1` is a thin wrapper piping through `find/1` and Ecto.Repo's `insert_or_update/1`.
`find/1` is also just a thin wrapper around `find_by/1` that assumes :id is the selector.
It's functions all the way down!
```elixir
%User{first_name: "Harry", last_name: "Potter"} |> find_by([:first_name, :last_name])
#=> {:found, %User{id: 4, first_name: "Harry", last_name: "Potter"}}
%User{name: "Buckbeak"} |> find_by(:name)
#=> {:not_found, %User{name: "Buckbeak"}}
# the following is equivalent
%User{id: 1} |> find_by(:id)
%User{id: 1} |> find
```
"""
# these macros decorate function calls by appending the repo as an addtional argument
defmacro __using__([repo: repo]) do
quote do
import unquote(__MODULE__), only: [
find_by: 2,
find: 1,
or_create: 1,
find_or_create_by: 2,
find_or_create: 1,
update_or_insert: 1,
upsert_by: 2,
upsert: 1
]
@repo unquote(repo)
end
end
defmacro find_by(record_struct, selectors) do
quote do
unquote(__MODULE__).find_by(unquote(record_struct), unquote(selectors), @repo)
end
end
defmacro find(record_struct) do
quote do
unquote(__MODULE__).find(unquote(record_struct), @repo)
end
end
defmacro or_create({status, record_struct}) do
quote do
unquote(__MODULE__).or_create({unquote(status), unquote(record_struct)}, @repo)
end
end
defmacro find_or_create_by(record_struct, selectors) do
quote do
unquote(__MODULE__).find_or_create_by(unquote(record_struct), unquote(selectors), @repo)
end
end
defmacro find_or_create(record_struct) do
quote do
unquote(__MODULE__).find_or_create(unquote(record_struct), @repo)
end
end
defmacro update_or_insert({status, record_struct}) do
quote do
unquote(__MODULE__).update_or_insert({unquote(status), unquote(record_struct)}, @repo)
end
end
defmacro upsert_by(record_struct, selectors) do
quote do
unquote(__MODULE__).upsert_by(unquote(record_struct), unquote(selectors), @repo)
end
end
defmacro upsert(record_struct) do
quote do
unquote(__MODULE__).upsert(unquote(record_struct), @repo)
end
end
def find_by(%_{} = _record_struct, nil, _repo), do: {:error, "find_by requires a list of fields to select on"}
def find_by(%_{} = _record_struct, [], _repo), do: {:error, "find_by requires a list of fields to select on"}
def find_by(%_{} = record_struct, selectors, repo) when is_list(selectors) do
with model = record_struct.__struct__,
where = selectors
|> Enum.reject(&Kernel.is_nil/1)
|> Enum.map(fn s -> {s, record_struct |> Map.get(s)} end),
found = model |> repo.get_by(where),
do: if found, do: {:found, found}, else: {:not_found, record_struct}
end
def find_by(%_{} = record_struct, selector, repo), do: find_by(record_struct, [selector], repo)
def find_by(_, _, _), do: {:error, "find_by/2 requires a resource struct and list of selectors arguments"}
def find(%_{id: _id} = record_struct, repo), do: record_struct |> find_by(:id, repo)
def find(%_{} = record_struct, repo), do: record_struct |> Map.merge(%{id: nil}) |> find_by(:id, repo)
def find(_, _), do: {:error, "find/1 requires a struct with an id field in the first argument"}
def or_create({:found, %_{} = found}, _repo), do: {:ok, found}
def or_create({:not_found, %_{} = record_struct}, repo), do: repo.insert(record_struct)
def or_create(error, _repo), do: error
def find_or_create_by(record_struct, selectors, repo), do: record_struct |> find_by(selectors, repo) |> or_create(repo)
def find_or_create(record_struct, repo), do: record_struct |> find(repo) |> or_create(repo)
def upsert_by(%_{} = record_struct, selectors, repo) do
model = record_struct.__struct__
case record_struct |> find_by(selectors, repo) do
{:found, found} -> found
{:not_found, _} -> struct(model)
end
|> model.changeset(record_struct |> Map.from_struct)
|> repo.insert_or_update
end
def upsert(%_{} = record_struct, repo), do: record_struct |> upsert_by(:id, repo)
end