edc4e51d1d
Closes #154
251 lines
7.1 KiB
Elixir
251 lines
7.1 KiB
Elixir
defmodule MusicLibrary.RecordSets do
|
|
@moduledoc """
|
|
User-curated record groupings with ordered items.
|
|
"""
|
|
|
|
import Ecto.Query, warn: false
|
|
|
|
alias MusicLibrary.RecordSets.{RecordSet, RecordSetItem}
|
|
alias MusicLibrary.Repo
|
|
|
|
@pagination Application.compile_env!(:music_library, :pagination)
|
|
|
|
@spec list_record_sets(MusicLibrary.Types.pagination_opts()) :: [RecordSet.t()]
|
|
def list_record_sets(opts \\ []) do
|
|
offset = Keyword.get(opts, :offset, 0)
|
|
limit = Keyword.get(opts, :limit, @pagination[:default_page_size])
|
|
|
|
from(rs in RecordSet,
|
|
order_by: [desc: rs.updated_at],
|
|
offset: ^offset,
|
|
limit: ^limit,
|
|
preload: [items: :record]
|
|
)
|
|
|> Repo.all()
|
|
end
|
|
|
|
@spec count_record_sets() :: non_neg_integer()
|
|
def count_record_sets do
|
|
Repo.aggregate(RecordSet, :count)
|
|
end
|
|
|
|
@spec count_record_sets(String.t()) :: non_neg_integer()
|
|
def count_record_sets(query) do
|
|
record_sets_search_query(query)
|
|
|> Repo.aggregate(:count)
|
|
end
|
|
|
|
@spec search_record_sets(String.t(), MusicLibrary.Types.pagination_opts()) :: [RecordSet.t()]
|
|
def search_record_sets(query, opts \\ []) do
|
|
offset = Keyword.get(opts, :offset, 0)
|
|
limit = Keyword.get(opts, :limit, @pagination[:default_page_size])
|
|
order = Keyword.get(opts, :order, :updated_at)
|
|
|
|
record_sets_search_query(query)
|
|
|> apply_order(order)
|
|
|> offset(^offset)
|
|
|> limit(^limit)
|
|
|> preload(items: :record)
|
|
|> Repo.all()
|
|
end
|
|
|
|
defp apply_order(query, :alphabetical) do
|
|
order_by(query, [rs], fragment("? COLLATE NOCASE ASC", rs.name))
|
|
end
|
|
|
|
defp apply_order(query, _) do
|
|
order_by(query, [rs], desc: rs.updated_at)
|
|
end
|
|
|
|
@spec get_record_set!(String.t()) :: RecordSet.t()
|
|
def get_record_set!(id) do
|
|
RecordSet
|
|
|> Repo.get!(id)
|
|
|> Repo.preload(items: :record)
|
|
end
|
|
|
|
@spec create_record_set(map()) :: {:ok, RecordSet.t()} | {:error, Ecto.Changeset.t()}
|
|
def create_record_set(attrs) do
|
|
%RecordSet{}
|
|
|> RecordSet.changeset(attrs)
|
|
|> Repo.insert()
|
|
|> case do
|
|
{:ok, record_set} -> {:ok, Repo.preload(record_set, items: :record)}
|
|
error -> error
|
|
end
|
|
end
|
|
|
|
@spec update_record_set(RecordSet.t(), map()) ::
|
|
{:ok, RecordSet.t()} | {:error, Ecto.Changeset.t()}
|
|
def update_record_set(%RecordSet{} = record_set, attrs) do
|
|
record_set
|
|
|> RecordSet.changeset(attrs)
|
|
|> Repo.update()
|
|
|> case do
|
|
{:ok, record_set} -> {:ok, Repo.preload(record_set, [items: :record], force: true)}
|
|
error -> error
|
|
end
|
|
end
|
|
|
|
@spec delete_record_set(RecordSet.t()) :: {:ok, RecordSet.t()} | {:error, Ecto.Changeset.t()}
|
|
def delete_record_set(%RecordSet{} = record_set) do
|
|
Repo.delete(record_set)
|
|
end
|
|
|
|
@spec change_record_set(RecordSet.t(), map()) :: Ecto.Changeset.t()
|
|
def change_record_set(%RecordSet{} = record_set, attrs \\ %{}) do
|
|
RecordSet.changeset(record_set, attrs)
|
|
end
|
|
|
|
@spec add_record_to_set(RecordSet.t(), String.t()) ::
|
|
{:ok, RecordSet.t()} | {:error, Ecto.Changeset.t()}
|
|
def add_record_to_set(%RecordSet{} = record_set, record_id) do
|
|
next_position =
|
|
from(i in RecordSetItem,
|
|
where: i.record_set_id == ^record_set.id,
|
|
select: coalesce(max(i.position), -1)
|
|
)
|
|
|> Repo.one!()
|
|
|> Kernel.+(1)
|
|
|
|
%RecordSetItem{}
|
|
|> RecordSetItem.changeset(%{position: next_position})
|
|
|> Ecto.Changeset.put_change(:record_set_id, record_set.id)
|
|
|> Ecto.Changeset.put_change(:record_id, record_id)
|
|
|> Ecto.Changeset.unique_constraint([:record_set_id, :record_id])
|
|
|> Repo.insert()
|
|
|> case do
|
|
{:ok, _item} -> {:ok, get_record_set!(record_set.id)}
|
|
error -> error
|
|
end
|
|
end
|
|
|
|
@spec remove_record_from_set(RecordSet.t(), String.t()) :: {:ok, RecordSet.t()}
|
|
def remove_record_from_set(%RecordSet{} = record_set, record_id) do
|
|
item =
|
|
from(i in RecordSetItem,
|
|
where: i.record_set_id == ^record_set.id and i.record_id == ^record_id
|
|
)
|
|
|> Repo.one!()
|
|
|
|
Repo.delete(item)
|
|
|
|
recompact_positions(record_set)
|
|
end
|
|
|
|
# sobelow_skip ["SQL.Query"]
|
|
@spec reorder_records_in_set(RecordSet.t(), [String.t()]) :: {:ok, RecordSet.t()}
|
|
def reorder_records_in_set(%RecordSet{} = record_set, ordered_record_ids)
|
|
when is_list(ordered_record_ids) do
|
|
{case_clauses, params} =
|
|
ordered_record_ids
|
|
|> Enum.with_index()
|
|
|> Enum.reduce({"", []}, fn {record_id, position}, {sql_acc, params_acc} ->
|
|
{"#{sql_acc}WHEN record_id = ? THEN ? ", params_acc ++ [record_id, position]}
|
|
end)
|
|
|
|
sql = """
|
|
UPDATE record_set_items
|
|
SET position = CASE #{case_clauses}ELSE position END
|
|
WHERE record_set_id = ?
|
|
"""
|
|
|
|
Repo.query(sql, params ++ [record_set.id])
|
|
|
|
{:ok, get_record_set!(record_set.id)}
|
|
end
|
|
|
|
@spec move_record_in_set(RecordSet.t(), String.t(), :up | :down) :: {:ok, RecordSet.t()}
|
|
def move_record_in_set(%RecordSet{} = record_set, record_id, direction)
|
|
when direction in [:up, :down] do
|
|
items =
|
|
from(i in RecordSetItem,
|
|
where: i.record_set_id == ^record_set.id,
|
|
order_by: [asc: i.position]
|
|
)
|
|
|> Repo.all()
|
|
|
|
index = Enum.find_index(items, fn i -> i.record_id == record_id end)
|
|
|
|
swap_index =
|
|
case direction do
|
|
:up -> index - 1
|
|
:down -> index + 1
|
|
end
|
|
|
|
if index && swap_index >= 0 && swap_index < length(items) do
|
|
item_a = Enum.at(items, index)
|
|
item_b = Enum.at(items, swap_index)
|
|
|
|
Repo.transaction(fn ->
|
|
item_a
|
|
|> Ecto.Changeset.change(position: item_b.position)
|
|
|> Repo.update!()
|
|
|
|
item_b
|
|
|> Ecto.Changeset.change(position: item_a.position)
|
|
|> Repo.update!()
|
|
end)
|
|
end
|
|
|
|
{:ok, get_record_set!(record_set.id)}
|
|
end
|
|
|
|
@spec list_record_sets_for_record(String.t()) :: [RecordSet.t()]
|
|
def list_record_sets_for_record(record_id) do
|
|
from(rs in RecordSet,
|
|
join: i in RecordSetItem,
|
|
on: i.record_set_id == rs.id,
|
|
where: i.record_id == ^record_id,
|
|
order_by: [asc: rs.name],
|
|
preload: [items: :record]
|
|
)
|
|
|> Repo.all()
|
|
end
|
|
|
|
defp record_sets_search_query(""), do: from(rs in RecordSet)
|
|
|
|
defp record_sets_search_query(query) do
|
|
like_query = "%#{query}%"
|
|
|
|
from(rs in RecordSet,
|
|
where:
|
|
like(rs.name, ^like_query) or
|
|
like(rs.description, ^like_query) or
|
|
fragment(
|
|
"""
|
|
EXISTS (
|
|
SELECT 1 FROM record_set_items AS i
|
|
JOIN records AS r ON r.id = i.record_id
|
|
WHERE i.record_set_id = ?
|
|
AND (
|
|
r.title LIKE ?
|
|
OR EXISTS (
|
|
SELECT 1 FROM json_each(r.artists)
|
|
WHERE json_extract(value, '$.name') LIKE ?
|
|
)
|
|
)
|
|
)
|
|
""",
|
|
rs.id,
|
|
^like_query,
|
|
^like_query
|
|
)
|
|
)
|
|
end
|
|
|
|
defp recompact_positions(record_set) do
|
|
record_set_id = record_set.id
|
|
|
|
record_ids =
|
|
from(i in RecordSetItem,
|
|
where: i.record_set_id == ^record_set_id,
|
|
order_by: [asc: i.position],
|
|
select: i.record_id
|
|
)
|
|
|> Repo.all()
|
|
|
|
reorder_records_in_set(record_set, record_ids)
|
|
end
|
|
end
|