Current section

Files

Jump to
endon lib endon.ex
Raw

lib/endon.ex

defmodule Endon do
@moduledoc ~S"""
Endon is an Elixir library that provides helper functions for [Ecto](https://hexdocs.pm/ecto/getting-started.html#content),
with inspiration from Ruby on Rails' [ActiveRecord](https://guides.rubyonrails.org/active_record_basics.html).
It's designed to be used within a module that is an Ecto.Schema and
provides helpful functions.
## Example
defmodule User do
use Endon
use Ecto.Schema
schema "users" do
field :name, :string
field :age, :integer, default: 0
has_many :posts, Post
end
end
Once Endon has been included, you can immediately use the helpful methods below. For instance:
iex> user = User.find(1)
iex> user = User.find_by(name: "billy")
iex> count = User.count()
iex> user = User.create!(name: "snake", age: 12)
iex> User.update!(user, age: 23)
iex> User.where([age: 23], preload: :posts)
"""
alias Endon.{Helpers}
@doc """
Fetches all entries from the data store matching the given query.
`opts` can be any of `:order_by`, `:preload`, or `:offset`
"""
@spec all(opts :: keyword()) :: list(Ecto.Schema.t())
def all(opts \\ []), do: doc!([opts])
@doc """
Fetch all entries that match the given conditions.
The conditions can be a `Ecto.Query.t/0` or a `Keyword.t/0`.
The options can be any of: `:limit`, `:order_by`, `:offset`, `:preload`.
## Examples
iex> User.where(id: 1)
iex> User.where(name: "billy", age: 23)
iex> User.where([name: "billy", age: 23], limit: 10, order_by: [desc: :id])
iex> query = from u in User, where: u.id > 10
iex> User.where(query, limit: 1)
"""
@spec where(keyword() | Ecto.Query.t(), keyword()) :: list(Ecto.Schema.t())
def where(conditions, opts \\ []), do: doc!([conditions, opts])
@doc """
Checks if there exists an entry that matches the given query.
`conditions` are the same as those accepted by `where/2`.
"""
@spec exists?(keyword() | Ecto.Query.t()) :: boolean()
def exists?(conditions), do: doc!([conditions])
@doc """
Fetches one or more structs from the data store based on the primary key(s) given.
Much like `fetch/2`, except an error is raised if the record(s) can't be found.
If one primary key is given, then one struct will be returned (or a `Endon.RecordNotFoundError`
raised if a match isn't found).
If more than one primary key is given in a list, then all of the structs with those ids
will be returned (and a `Endon.RecordNotFoundError` will be raised if any one of the primary
keys can't be found).
The only option that matters here is `:preload`.
"""
@spec find(integer() | list(integer()), keyword()) :: list(Ecto.Schema.t()) | Ecto.Schema.t()
def find(id_or_ids, opts \\ []), do: doc!([id_or_ids, opts])
@doc """
Fetches one or more structs from the data store based on the primary key(s) given.
If one primary key is given, then one struct will be returned (or `:error` if not found)
If more than one primary key is given in a list, then all of the structs with those ids
will be returned (and `:error` will be returned if any one of the primary
keys can't be found).
The only option that matters here is `:preload`.
"""
@spec fetch(integer() | list(integer()), keyword()) ::
{:ok, list(Ecto.Schema.t())} | {:ok, Ecto.Schema.t()} | :error
def fetch(id_or_ids, opts \\ []), do: doc!([id_or_ids, opts])
@doc """
Find or create a record based on specific attributes values.
Similar to `find_by`, except that if a record cannot be found with the given attributes
then a new one will be created.
Returns `{:ok, struct}` if one is found/created, or `{:error, changeset}` if there is
a validation error.
"""
@spec find_or_create_by(keyword() | struct()) :: Ecto.Schema.t()
def find_or_create_by(params), do: doc!([params])
@doc """
Find a single record based on given conditions.
If a record can't be found, then `nil` is returned.
The only option that matters here is `:preload`.
"""
@spec find_by(keyword(), keyword()) :: Ecto.Schema.t() | nil
def find_by(conditions, opts \\ []), do: doc!([conditions, opts])
@doc """
Create a `Stream` that queries the data store in batches for matching records.
This is useful for paginating through a very large result set in chunks. The `Stream`
is a composable, lazy enumerable that allows you to iterate through what could be a
very large number of records efficiently.
`opts` can be any of:
* batch_size: Specifies the size of the batch. Defaults to 1000.
* start: Specifies the primary key value to start from, inclusive of the value.
* finish: Specifies the primary key value to end at, inclusive of the value.
And `conditions` are anyting accepted by `where/2` (including a `Ecto.Query.t/0`).
This function will only work for types that have a primary key that is an integer.
## Examples
iex> Enum.each(User.stream_where(), &User.do_some_processing/1)
iex> query = from u in User, where: u.id > 100
iex> Enum.each(User.stream_where(query, batch_size: 10), fn user ->
iex> User.do_some_processing(user)
iex> end)
"""
@spec stream_where(keyword(), keyword()) :: Enumerable.t()
def stream_where(conditions \\ [], opts \\ []), do: doc!([conditions, opts])
@doc """
Get a count of all records matching the conditions. You can give an optional column;
if none is specified, then it's the equivalent of a `select count(*)`.
`conditions` are anyting accepted by `where/2` (including a `Ecto.Query.t/0`).
"""
@spec count(atom() | nil, keyword() | Ecto.Query.t()) :: integer()
def count(column \\ nil, conditions \\ []), do: doc!([column, conditions])
@doc """
Calculate the given aggregate over the given column.
`conditions` are anyting accepted by `where/2` (including a `Ecto.Query.t/0`).
"""
@spec aggregate(atom(), :avg | :count | :max | :min | :sum, keyword() | Ecto.Query.t()) ::
term() | nil
def aggregate(column, aggregate, conditions \\ []), do: doc!([column, aggregate, conditions])
@doc """
Get the sum of a given column.
`conditions` are anyting accepted by `where/2` (including a `Ecto.Query.t/0`).
"""
@spec sum(String.t() | atom(), keyword()) :: integer()
def sum(column, conditions \\ []), do: doc!([column, conditions])
@doc """
Get the average of a given column.
`conditions` are anyting accepted by `where/2` (including a `Ecto.Query.t/0`).
"""
@spec avg(String.t() | atom(), keyword()) :: float()
def avg(column, conditions \\ []), do: doc!([column, conditions])
@doc """
Get the minimum value of a given column.
`conditions` are anyting accepted by `where/2` (including a `Ecto.Query.t/0`).
"""
@spec min(String.t() | atom(), keyword()) :: float() | integer()
def min(column, conditions \\ []), do: doc!([column, conditions])
@doc """
Get the maximum value of a given column.
`conditions` are anyting accepted by `where/2` (including a `Ecto.Query.t/0`).
"""
@spec max(String.t() | atom(), keyword()) :: float() | integer()
def max(column, conditions \\ []), do: doc!([column, conditions])
@doc """
Insert a new record into the data store.
`params` can be either a `Keyword` list or `Map` of attributes and values.
Returns `{:ok, struct}` if one is created, or `{:error, changeset}` if there is
a validation error.
"""
@spec create(keyword() | struct()) :: {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()}
def create(params), do: doc!([params])
@doc """
Insert a new record into the data store.
`params` can be either a `Keyword` list or `Map` of attributes and values.
Returns the struct if created, or raises a `Endon.ValidationError` if there was
a validation error.
"""
@spec create!(keyword() | struct()) :: Ecto.Schema.t()
def create!(params), do: doc!([params])
@doc """
Update a record in the data store.
The `struct` must be a `Ecto.Schema.t/0` (your module that uses `Ecto.Schema`).
`params` can be either a `Keyword` list or `Map` of attributes and values.
Returns `{:ok, struct}` if one is created, or `{:error, changeset}` if there is
a validation error.
"""
@spec update(Ecto.Schema.t(), keyword() | struct()) ::
{:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()}
def update(struct, params), do: doc!([struct, params])
@doc """
Update a record in the data store.
The `struct` must be a `Ecto.Schema.t/0` (your module that uses `Ecto.Schema`).
`params` can be either a `Keyword` list or `Map` of attributes and values.
Returns the struct if it was updated, or raises a `Endon.ValidationError` if there was
a validation error.
"""
@spec update!(Ecto.Schema.t(), keyword() | struct()) :: Ecto.Schema.t()
def update!(struct, params), do: doc!([struct, params])
@doc """
Update multiple records in the data store based on conditions.
Update all the records that match the given `conditions`, setting the given `params`
as attributes. `params` can be either a `Keyword` list or `Map` of attributes and values,
and `conditions` is the same as for `where/2`.
It returns a tuple containing the number of entries and any returned result as second element.
The second element is nil by default unless a select is supplied in the update query.
"""
@spec update_where(keyword(), keyword() | Ecto.Query.t()) :: {integer(), nil | [term()]}
def update_where(params, conditions \\ []), do: doc!([params, conditions])
@doc """
Delete a record in the data store.
The `struct` must be a `Ecto.Schema.t/0` (your module that uses `Ecto.Schema`).
Returns `{:ok, struct}` if the record is deleted, or `{:error, changeset}` if there is
a validation error.
"""
@spec delete(Ecto.Schema.t()) :: {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()}
def delete(struct), do: doc!([struct])
@doc """
Delete a record in the data store.
The `struct` must be a `Ecto.Schema.t/0` (your module that uses `Ecto.Schema`).
Returns the struct if it was deleted, or raises a `Endon.ValidationError` if there was
a validation error.
"""
@spec delete!(Ecto.Schema.t()) :: Ecto.Schema.t()
def delete!(struct), do: doc!([struct])
@doc """
Delete multiple records in the data store based on conditions.
Delete all the records that match the given `conditions` (the same as for `where/2`).
**Note:** If you don't supply any conditions, _all_ records will be deleted.
It returns a tuple containing the number of entries and any returned result as second element.
The second element is nil by default unless a select is supplied in the update query.
"""
@spec delete_where(keyword()) :: {integer(), nil | [term()]}
def delete_where(conditions \\ []), do: doc!([conditions])
@doc """
Get the first record, or `nil` if none are found.
Find the first record (or first N records if a `:limit` opt parameter is supplied).
If no order is defined it will order by primary key.
`opts` can include `:limit` and `:order_by` (by default, orders by primary key ascending).
`conditions` can be anyting accepted by `where/2` (including a `Ecto.Query.t/0`).
"""
@spec first(keyword(), keyword()) :: Ecto.Schema.t() | nil
def first(conditions \\ [], opts \\ []), do: doc!([conditions, opts])
@doc """
Get the last record, or `nil` if none are found.
Find the last record (or last N records if a `:limit` opt parameter is supplied).
If no order is defined it will order by primary key descending.
`opts` can include `:limit` and `:order_by` (by default, orders by primary key descending).
`conditions` can be anyting accepted by `where/2` (including a `Ecto.Query.t/0`).
"""
@spec last(keyword(), keyword()) :: Ecto.Schema.t() | nil
def last(conditions \\ [], opts \\ []), do: doc!([conditions, opts])
@doc """
Take a query and add conditions (the same as `where/2` accepts).
This will not actually run the query, so you will need
to pass the result to `where/2` or `Ecto.Repo.all/2`/`Ecto.Repo.one/2`.
For instance:
existing_query = from x in Post
Post.scope(existing_query, id: 1) |> Post.first()
This is just a helpful function to make adding conditions easier to an existing query.
"""
@spec scope(Ecto.Query.t(), keyword()) :: Ecto.Query.t()
def scope(query, conditions), do: doc!([query, conditions])
@doc """
Create a query with the given conditions (the same as `where/2` accepts).
This will not actually run the query, so you will need
to pass the result to `where/2` or `Ecto.Repo.all/2`/`Ecto.Repo.one/2`.
For instance, this will just run one query to find a record with id 1 with name Bill.
Post.scope(id: 1) |> Post.scope(name: 'Bill') |> Post.first()
This is just a helpful function to make adding conditions easier to an existing `Ecto.Schema`
"""
@spec scope(keyword()) :: Ecto.Query.t()
def scope(conditions), do: doc!([conditions])
defp doc!(_) do
raise "The functions in Endon should not be invoked directly, they're for docs only"
end
defmacro __using__(opts \\ []) do
repo = Keyword.get(opts, :repo, Application.get_env(:endon, :repo))
quote bind_quoted: [repo: repo] do
@repo repo
def scope(query, conditions), do: Helpers.scope(query, conditions)
def scope(conditions), do: Helpers.scope(__MODULE__, conditions)
def all(opts \\ []), do: Helpers.all(@repo, __MODULE__, opts)
def where(conditions, opts \\ []), do: Helpers.where(@repo, __MODULE__, conditions, opts)
def exists?(conditions), do: Helpers.exists?(@repo, __MODULE__, conditions)
def find(id_or_ids, opts \\ []), do: Helpers.find(@repo, __MODULE__, id_or_ids, opts)
def fetch(id_or_ids, opts \\ []), do: Helpers.fetch(@repo, __MODULE__, id_or_ids, opts)
def find_or_create_by(%{} = params),
do: Helpers.find_or_create_by(@repo, __MODULE__, params)
def find_or_create_by(params) when is_list(params),
do: find_or_create_by(Enum.into(params, %{}))
def find_by(conditions, opts \\ []),
do: Helpers.find_by(@repo, __MODULE__, conditions, opts)
def stream_where(conditions \\ [], opts \\ []),
do: Helpers.stream_where(@repo, __MODULE__, conditions, opts)
def aggregate(column, aggregate, conditions \\ []),
do: Helpers.aggregate(@repo, __MODULE__, column, aggregate, conditions)
def count(column \\ nil, conditions \\ [])
def count(nil, conditions), do: Helpers.count(@repo, __MODULE__, conditions)
def count(column, conditions), do: aggregate(column, :count, conditions)
def sum(column, conditions \\ []), do: aggregate(column, :sum, conditions)
def avg(column, conditions \\ []), do: aggregate(column, :avg, conditions)
def min(column, conditions \\ []), do: aggregate(column, :min, conditions)
def max(column, conditions \\ []), do: aggregate(column, :max, conditions)
def create(%{} = params), do: Helpers.create(@repo, __MODULE__, params)
def create(params) when is_list(params), do: create(Enum.into(params, %{}))
def create!(%{} = params), do: Helpers.create!(@repo, __MODULE__, params)
def create!(params) when is_list(params), do: create!(Enum.into(params, %{}))
def update(%{} = struct, %{} = params),
do: Helpers.update(@repo, __MODULE__, struct, params)
def update(%{} = struct, params) when is_list(params),
do: update(struct, Enum.into(params, %{}))
def update!(%{} = struct, %{} = params),
do: Helpers.update!(@repo, __MODULE__, struct, params)
def update!(%{} = struct, params) when is_list(params),
do: update!(struct, Enum.into(params, %{}))
def update_where(params, conditions \\ []),
do: Helpers.update_where(@repo, __MODULE__, params, conditions)
def delete(%{} = struct),
do: Helpers.delete(@repo, __MODULE__, struct)
def delete!(%{} = struct),
do: Helpers.delete!(@repo, __MODULE__, struct)
def delete_where(conditions \\ []), do: Helpers.delete_where(@repo, __MODULE__, conditions)
def first(conditions \\ [], opts \\ []),
do: Helpers.first(@repo, __MODULE__, conditions, opts)
def last(conditions \\ [], opts \\ []),
do: Helpers.last(@repo, __MODULE__, conditions, opts)
end
end
end