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 some 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. See the [overview](overview.html) and [features page](features.html) for examples. """ alias Endon.Helpers @doc """ Fetches all entries from the data store matching the given query. Limit results to those matching these conditions. Value can be anyting accepted by `where/2` (including a `t:Ecto.Query.t/0`). ## Options * `:order_by` - By default, orders by primary key ascending * `:preload` - A list of fields to preload, much like `Ecto.Repo.preload/3` * `:offset` - Number to offset by """ @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 `t:Ecto.Query.t/0` or a `t:Keyword.t/0`. ## Options * `:order_by` - By default, orders by primary key ascending * `:preload` - A list of fields to preload, much like `Ecto.Repo.preload/3` * `:offset` - Number to offset by * `:limit` - Limit results to the given count ## 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 `Ecto.NoResultsError` 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 `Ecto.NoResultsError` will be raised if any one of the primary keys can't be found). ## Options * `:preload` - A list of fields to preload, much like `Ecto.Repo.preload/3` """ @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). ## Options * `:preload` - A list of fields to preload, much like `Ecto.Repo.preload/3` """ @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. ## Options * `:preload` - A list of fields to preload, much like `Ecto.Repo.preload/3` """ @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. The `conditions` are anyting accepted by `where/2` (including a `t:Ecto.Query.t/0`). This function will only work for types that have a primary key that is an integer. ## Options * `: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. ## 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 `t: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 `t: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 `t: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 `t: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 `t: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 `t: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 `Ecto.InvalidChangesetError` 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 `t: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 `t: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 `Ecto.InvalidChangesetError` 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 `t: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 `t:Ecto.Schema.t/0` (your module that uses `Ecto.Schema`). Returns the struct if it was deleted, or raises a `Ecto.InvalidChangesetError` 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. ## Examples # this line using Ecto.Repo from(p in Post, where: p.user_id == 123) |> MyRepo.delete_all # is the same as this line in Endon Post.delete_where(user_id: 123) """ @spec delete_where(keyword()) :: {integer(), nil | [term()]} def delete_where(conditions \\ []), do: doc!([conditions]) @doc """ Get the first `count` records. If you ask for one thing (`count` of 1), you will get back the first record or `nil` if none are found. If you ask for more than one thing (`count` > 1), you'll get back a list of 0 or more records. If no order is defined it will order by primary key ascending. ## Options * `:order_by` - By default, orders by primary key descending * `:conditions` - Limit results to those matching these conditions. Value can be anyting accepted by `where/2` (including a `t:Ecto.Query.t/0`). ## Examples # get the first 3 posts, will return a list posts = Post.first(3) # get the first post, will return one item (or nil if none found) post = Post.first() # get the first 3 posts by author id 1 posts = Post.first(3, conditions: [author_id: 1]) """ @spec first(integer(), keyword()) :: [Ecto.Schema.t()] | Ecto.Schema.t() | nil def first(count \\ 1, opts \\ []), do: doc!([count, opts]) @doc """ Get the last `count` records. If you ask for one thing (`count` of 1), you will get back the last record or `nil` if none are found. If you ask for more than one thing (`count` > 1), you'll get back a list of 0 or more records. If no order is defined it will order by primary key descending. ## Options * `:order_by` - By default, orders by primary key descending * `:conditions` - Limit results to those matching these conditions. Value can be anyting accepted by `where/2` (including a `t:Ecto.Query.t/0`). ## Examples # get the last 3 posts, will return a list posts = Post.last(3) # get the last post, will return one item post = Post.last() # get the last 3 posts by author id 1 posts = Post.last(3, conditions: [author_id: 1]) """ @spec last(integer(), keyword()) :: [Ecto.Schema.t()] | Ecto.Schema.t() | nil def last(count \\ 1, opts \\ []), do: doc!([count, 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 aggregate(column, aggregate, conditions \\ []), do: Helpers.aggregate(@repo, __MODULE__, column, aggregate, conditions) def all(opts \\ []), do: Helpers.all(@repo, __MODULE__, opts) def avg(column, conditions \\ []), do: aggregate(column, :avg, 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 create(params), do: Helpers.create(@repo, __MODULE__, params) def create!(params), do: Helpers.create!(@repo, __MODULE__, params) 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 exists?(conditions), do: Helpers.exists?(@repo, __MODULE__, conditions) def fetch(id_or_ids, opts \\ []), do: Helpers.fetch(@repo, __MODULE__, id_or_ids, opts) def find(id_or_ids, opts \\ []), do: Helpers.find(@repo, __MODULE__, id_or_ids, opts) def find_by(conditions, opts \\ []), do: Helpers.find_by(@repo, __MODULE__, conditions, opts) def find_or_create_by(params), do: Helpers.find_or_create_by(@repo, __MODULE__, params) def first(count \\ 1, opts \\ []) def first(count, opts), do: Helpers.first(@repo, __MODULE__, count, opts) def last(count \\ 1, opts \\ []) def last(count, opts), do: Helpers.last(@repo, __MODULE__, count, opts) def max(column, conditions \\ []), do: aggregate(column, :max, conditions) def min(column, conditions \\ []), do: aggregate(column, :min, conditions) def scope(query, conditions), do: Helpers.scope(query, conditions) def scope(conditions), do: scope(__MODULE__, conditions) def stream_where(conditions \\ [], opts \\ []), do: Helpers.stream_where(@repo, __MODULE__, conditions, opts) def sum(column, conditions \\ []), do: aggregate(column, :sum, conditions) def update(%{} = struct, params), do: Helpers.update(@repo, __MODULE__, struct, params) def update!(%{} = struct, params), do: Helpers.update!(@repo, __MODULE__, struct, params) def update_where(params, conditions \\ []), do: Helpers.update_where(@repo, __MODULE__, params, conditions) def where(conditions, opts \\ []), do: Helpers.where(@repo, __MODULE__, conditions, opts) end end end