Skip to content

Commit

Permalink
Merge pull request #23 from dwyl/changesets
Browse files Browse the repository at this point in the history
Changesets
  • Loading branch information
nelsonic authored Dec 6, 2018
2 parents 4cbf9d7 + d460f67 commit 95bd86f
Show file tree
Hide file tree
Showing 13 changed files with 415 additions and 205 deletions.
107 changes: 81 additions & 26 deletions lib/alog.ex
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,14 @@ defmodule Alog do
field(:entry_id, :string)
"""

@callback insert(map()) :: {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()}
@callback insert(Ecto.Schema.t() | Ecto.Changeset.t()) ::
{:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()}
@callback get(String.t()) :: Ecto.Schema.t() | nil | no_return()
@callback get_by(Keyword.t() | map()) :: Ecto.Schema.t() | nil | no_return()
@callback update(Ecto.Schema.t(), map()) ::
{:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()}
@callback update(Ecto.Changeset.t()) :: {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()}
@callback get_history(Ecto.Schema.t()) :: [Ecto.Schema.t()] | no_return()
@callback delete(Ecto.Schema.t()) :: {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()}
@callback delete(Ecto.Schema.t() | Ecto.Changeset.t()) ::
{:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()}
@callback preload(Ecto.Schema.t() | list(Ecto.Schema.t()), atom() | list()) ::
Ecto.Schema.t() | list(Ecto.Schema.t())

Expand Down Expand Up @@ -85,15 +86,23 @@ defmodule Alog do
end

@doc """
Applies a schema's changeset function and inserts it into the database.
Inserts a struct made with a schema or a changeset into the database.
Adds an entry id to link it to future updates of the item.
User.insert(attributes)
If `cast_assoc` has been used on the changeset before passing it to this function,
any nested associations will also be given an `entry_id` before they are
inserted into the database.
%User{name: "username", age: "25"}
|> User.insert()
%User{}
|> User.changeset(%{name: "username", age: "25"})
|> User.insert()
"""
def insert(attrs) do
%__MODULE__{}
def insert(struct_or_changeset) do
struct_or_changeset
|> insert_entry_id()
|> __MODULE__.changeset(attrs)
|> @repo.insert()
end

Expand Down Expand Up @@ -145,20 +154,30 @@ defmodule Alog do
Updates an item in the database.
Copies the current row, updates the relevant fields and appends
it to the database table.
Requires a changeset to be given.
User.get("5ds4fg31-a7f1-2hd8-x56a-d4s3g7ded1vv2")
|> User.update(%{age: 44})
|> User.changeset(%{age: 44})
|> User.update()
"""
def update(%__MODULE__{} = item, attrs) do
item
|> @repo.preload(__MODULE__.__schema__(:associations))
|> Map.put(:id, nil)
|> Map.put(:inserted_at, nil)
|> Map.put(:updated_at, nil)
|> __MODULE__.changeset(attrs)
def update(%Ecto.Changeset{} = changeset) do
data =
changeset
|> Map.get(:data)
|> Map.put(:id, nil)
|> Map.put(:inserted_at, nil)
|> Map.put(:updated_at, nil)
|> @repo.preload(__MODULE__.__schema__(:associations))

changeset
|> Map.put(:data, data)
|> @repo.insert()
end

def update(_) do
raise ArgumentError, "The argument provided to update/1 must be an Ecto.Changeset"
end

@doc """
Gets the full history of an item in the database.
Expand Down Expand Up @@ -199,15 +218,21 @@ defmodule Alog do
User.get("5ds4fg31-a7f1-2hd8-x56a-d4s3g7ded1vv2")
|> User.delete()
User.get("5ds4fg31-a7f1-2hd8-x56a-d4s3g7ded1vv2")
|> User.changeset(%{})
|> User.delete()
"""
def delete(item) do
item
|> @repo.preload(__MODULE__.__schema__(:associations))
|> Map.put(:id, nil)
|> Map.put(:inserted_at, nil)
|> Map.put(:updated_at, nil)
|> __MODULE__.changeset(%{deleted: true})
|> @repo.insert()
def delete(%Ecto.Changeset{} = changeset) do
changeset
|> Ecto.Changeset.put_change(:deleted, true)
|> update()
end

def delete(%__MODULE__{} = entry) do
entry
|> Ecto.Changeset.cast(%{deleted: true}, [:deleted])
|> update()
end

@doc """
Expand Down Expand Up @@ -264,13 +289,43 @@ defmodule Alog do
from(m in subquery(sub), where: not m.deleted, select: m)
end

defp insert_entry_id(entry) do
defp insert_entry_id(%Ecto.Changeset{} = entry) do
with {:ok, nil} <- Map.fetch(entry.data, :entry_id),
nil <- get_change(entry, :entry_id) do
entry
|> put_change(:entry_id, Ecto.UUID.generate())
|> insert_nested_entry_ids()
else
_ ->
entry
end
end

defp insert_entry_id(%__MODULE__{} = entry) do
case Map.fetch(entry, :entry_id) do
{:ok, nil} -> %{entry | entry_id: Ecto.UUID.generate()}
_ -> entry
end
end

defp insert_nested_entry_ids(changeset) do
assocs = changeset.data.__struct__.__schema__(:associations)

Enum.reduce(changeset.changes, changeset, fn {k, v}, acc ->
if k in assocs do
assoc =
case v do
l when is_list(l) -> Enum.map(l, &insert_entry_id/1)
item -> insert_entry_id(item)
end

Ecto.Changeset.put_change(acc, k, assoc)
else
acc
end
end)
end

defoverridable Alog
end
end
Expand Down
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ defmodule Alog.MixProject do
def project do
[
app: :alog,
version: "0.1.0",
version: "0.3.0",
elixir: "~> 1.7",
elixirc_paths: elixirc_paths(Mix.env()),
start_permanent: Mix.env() == :prod,
Expand Down
22 changes: 22 additions & 0 deletions test/all_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
defmodule AlogTest.AllTest do
use Alog.TestApp.DataCase

alias Alog.TestApp.{User, Helpers}

describe "all/0:" do
test "succeeds" do
{:ok, _} = %User{} |> User.changeset(Helpers.user_1_params()) |> User.insert()
{:ok, _} = %User{} |> User.changeset(Helpers.user_2_params()) |> User.insert()

assert length(User.all()) == 2
end

test "does not include old items" do
{:ok, user} = %User{} |> User.changeset(Helpers.user_1_params()) |> User.insert()
{:ok, _} = %User{} |> User.changeset(Helpers.user_2_params()) |> User.insert()
{:ok, _} = user |> User.changeset(%{postcode: "W2 3EC"}) |> User.update()

assert length(User.all()) == 2
end
end
end
170 changes: 0 additions & 170 deletions test/alog_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -2,104 +2,6 @@ defmodule AlogTest do
use Alog.TestApp.DataCase
doctest Alog

alias Alog.TestApp.{User, Item, Helpers}

describe "insert/1:" do
test "succeeds" do
assert {:ok, user} =
User.insert(%{name: "Thor", username: "gdofthndr12", postcode: "E2 0SY"})
end

test "validates required fields" do
{:error, changeset} = User.insert(%{name: "Thor"})

assert length(changeset.errors) > 0
end

test "inserted user is available" do
{:ok, user} = User.insert(%{name: "Thor", username: "gdofthndr12", postcode: "E2 0SY"})

assert User.get(user.entry_id) == user
end
end

describe "update/2:" do
test "succeeds" do
{:ok, user} = User.insert(%{name: "Thor", username: "gdofthndr12", postcode: "E2 0SY"})
assert {:ok, updated_user} = User.update(user, %{postcode: "W2 3EC"})
end

test "'get' returns most recently updated item" do
{:ok, user} = User.insert(%{name: "Thor", username: "gdofthndr12", postcode: "E2 0SY"})
{:ok, updated_user} = User.update(user, %{postcode: "W2 3EC"})

assert User.get(user.entry_id) |> User.preload(:items) == updated_user
end
end

describe "get_history/1:" do
test "gets all items" do
{:ok, user} = User.insert(%{name: "Thor", username: "gdofthndr12", postcode: "E2 0SY"})
{:ok, updated_user} = User.update(user, %{postcode: "W2 3EC"})

assert length(User.get_history(updated_user)) == 2
end
end

describe "all/0:" do
test "succeeds" do
{:ok, _} = User.insert(%{name: "Thor", username: "gdofthndr12", postcode: "E2 0SY"})
{:ok, _} = User.insert(%{name: "Loki", username: "mschfmkr", postcode: "E1 6DR"})

assert length(User.all()) == 2
end

test "does not include old items" do
{:ok, user} = User.insert(%{name: "Thor", username: "gdofthndr12", postcode: "E2 0SY"})
{:ok, _} = User.insert(%{name: "Loki", username: "mschfmkr", postcode: "E1 6DR"})
{:ok, _} = User.update(user, %{postcode: "W2 3EC"})

assert length(User.all()) == 2
end
end

describe "delete/1:" do
test "succeeds" do
{:ok, user} = User.insert(%{name: "Thor", username: "gdofthndr12", postcode: "E2 0SY"})
assert {:ok, _} = User.delete(user)
end

test "deleted items are not retrieved with 'get'" do
{:ok, user} = User.insert(%{name: "Thor", username: "gdofthndr12", postcode: "E2 0SY"})
{:ok, _} = User.delete(user)

assert User.get(user.entry_id) == nil
end

test "deleted items are not retrieved with 'all'" do
{:ok, user} = User.insert(%{name: "Thor", username: "gdofthndr12", postcode: "E2 0SY"})
{:ok, _} = User.delete(user)

assert length(User.all()) == 0
end
end

describe "get_by/2:" do
test "only returns one result" do
{:ok, user} = User.insert(%{name: "Thor", username: "gdofthndr12", postcode: "E2 0SY"})
{:ok, user_2} = User.insert(%{name: "Loki", username: "mschfmkr", postcode: "E2 0SY"})

assert User.get_by(postcode: "E2 0SY") == user_2
end

test "works with multiple clauses" do
{:ok, user} = User.insert(%{name: "Thor", username: "gdofthndr12", postcode: "E2 0SY"})
{:ok, user_2} = User.insert(%{name: "Loki", username: "mschfmkr", postcode: "E2 0SY"})

assert User.get_by(postcode: "E2 0SY", name: "Thor") == user
end
end

describe "required fields" do
test "schema without delete field raises error" do
assert_raise RuntimeError, fn ->
Expand Down Expand Up @@ -159,76 +61,4 @@ defmodule AlogTest do
end).()
end
end

describe "preload/2:" do
test "preloads many_to_many associations" do
{:ok, _, item} = Helpers.seed_data()

# item types are not loaded by default
assert_raise ArgumentError, fn ->
item.entry_id
|> Item.get()
|> Map.get(:item_types)
|> length()
end

assert item.entry_id
|> Item.get()
|> Item.preload(:item_types)
|> Map.get(:item_types)
|> length() == 1
end

test "preloads one_to_many associations" do
{:ok, user, _} = Helpers.seed_data()

# items are not loaded by default
assert_raise ArgumentError, fn ->
user.entry_id
|> User.get()
|> Map.get(:items)
|> length()
end

assert user.entry_id
|> User.get()
|> User.preload(:items)
|> Map.get(:items)
|> length() == 1
end

test "preloads nested associations" do
{:ok, user, item} = Helpers.seed_data()

# item_types are not loaded by default
assert_raise ArgumentError, fn ->
item.entry_id
|> Item.get()
|> Map.get(:item_types)
|> length()
end

assert user.entry_id
|> User.get()
|> User.preload(items: [:item_types])
|> Map.get(:items)
|> List.first()
|> Map.get(:item_types)
|> length() == 1
end

test "preloads two level deep nested associations" do
{:ok, user, _} = Helpers.seed_data()

assert user.entry_id
|> User.get()
|> User.preload(items: [item_types: [:items]])
|> Map.get(:items)
|> List.first()
|> Map.get(:item_types)
|> List.first()
|> Map.get(:items)
|> length() == 2
end
end
end
Loading

0 comments on commit 95bd86f

Please sign in to comment.