From 838e99195fbc41b3968d1b39d34cc0bf2f048c20 Mon Sep 17 00:00:00 2001 From: Danielwhyte Date: Tue, 30 Oct 2018 10:53:03 +0000 Subject: [PATCH 1/2] adds tests for preloading --- config/test.exs | 5 ++ lib/alog.ex | 20 ++--- .../20181026080544_create_items.exs | 28 +++++++ test/alog_test.exs | 84 +++++++++++++++++-- test/support/helpers.ex | 42 ++++++++++ test/support/item.ex | 29 +++++++ test/support/item_type.ex | 27 ++++++ test/support/user.ex | 2 + 8 files changed, 221 insertions(+), 16 deletions(-) create mode 100644 priv/repo/test_app/migrations/20181026080544_create_items.exs create mode 100644 test/support/helpers.ex create mode 100644 test/support/item.ex create mode 100644 test/support/item_type.ex diff --git a/config/test.exs b/config/test.exs index 9cf0680..16720f2 100644 --- a/config/test.exs +++ b/config/test.exs @@ -10,3 +10,8 @@ config :alog, Alog.Repo, priv: "priv/repo/test_app/" config :alog, ecto_repos: [Alog.Repo] + +config :logger, :console, + format: "$time $metadata[$level] $message\n", + metadata: [:request_id], + level: :warn diff --git a/lib/alog.ex b/lib/alog.ex index e7aa0b8..51065b9 100644 --- a/lib/alog.ex +++ b/lib/alog.ex @@ -187,16 +187,6 @@ defmodule Alog do |> @repo.insert() end - @doc """ - Preloads an item's (or list of items') association. - - User.get("5ds4fg31-a7f1-2hd8-x56a-d4s3g7ded1vv2") - |> User.preload(:friends) - """ - def preload(item, assoc) do - @repo.preload(item, [{assoc, preload_query(assoc)}]) - end - @doc """ Preloads an item's (or list of items') multiple associations. Also recursively preloads any nested associations. @@ -218,6 +208,16 @@ defmodule Alog do ) end + @doc """ + Preloads an item's (or list of items') association. + + User.get("5ds4fg31-a7f1-2hd8-x56a-d4s3g7ded1vv2") + |> User.preload(:friends) + """ + def preload(item, assoc) do + @repo.preload(item, [{assoc, preload_query(assoc)}]) + end + defp preload_map(assoc, owner) do case assoc do {k, v} -> diff --git a/priv/repo/test_app/migrations/20181026080544_create_items.exs b/priv/repo/test_app/migrations/20181026080544_create_items.exs new file mode 100644 index 0000000..3bc37ae --- /dev/null +++ b/priv/repo/test_app/migrations/20181026080544_create_items.exs @@ -0,0 +1,28 @@ +defmodule Alog.Repo.Migrations.CreateItems do + use Ecto.Migration + + def change do + create table(:items) do + add(:name, :string) + add(:entry_id, :string) + add(:deleted, :boolean, default: false) + add(:owner, references(:users)) + + timestamps() + end + + create table(:item_types) do + add(:type, :string) + add(:entry_id, :string) + add(:deleted, :boolean, default: false) + + timestamps() + end + + create table(:items_item_types, primary_key: false) do + add(:item_id, references(:items, on_delete: :delete_all, column: :id, type: :id)) + + add(:item_type_id, references(:item_types, on_delete: :delete_all, column: :id, type: :id)) + end + end +end diff --git a/test/alog_test.exs b/test/alog_test.exs index 1469150..8c017b5 100644 --- a/test/alog_test.exs +++ b/test/alog_test.exs @@ -2,7 +2,7 @@ defmodule AlogTest do use Alog.TestApp.DataCase doctest Alog - alias Alog.TestApp.User + alias Alog.TestApp.{User, Item, Helpers} describe "insert/1:" do test "succeeds" do @@ -33,7 +33,7 @@ defmodule AlogTest 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) == updated_user + assert User.get(user.entry_id) |> User.preload(:items) == updated_user end end @@ -87,7 +87,7 @@ defmodule AlogTest do describe "required fields" do test "schema without delete field raises error" do assert_raise RuntimeError, fn -> - defmodule BadSchema do + defmodule NoDeleteSchema do use Ecto.Schema use Alog @@ -101,7 +101,7 @@ defmodule AlogTest do test "schema without entry_id field raises error" do assert_raise RuntimeError, fn -> - defmodule BadSchema do + defmodule NoEntrySchema do use Ecto.Schema use Alog @@ -115,7 +115,7 @@ defmodule AlogTest do test "schema with deleted field of wrong type raises error" do assert_raise RuntimeError, fn -> - defmodule BadSchema do + defmodule BadDeletedSchema do use Ecto.Schema use Alog @@ -130,7 +130,7 @@ defmodule AlogTest do test "both required fields do not raise error" do assert (fn -> - defmodule BadSchema do + defmodule GoodSchema do use Ecto.Schema use Alog @@ -143,4 +143,76 @@ 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 diff --git a/test/support/helpers.ex b/test/support/helpers.ex new file mode 100644 index 0000000..741e81e --- /dev/null +++ b/test/support/helpers.ex @@ -0,0 +1,42 @@ +defmodule Alog.TestApp.Helpers do + alias Alog.TestApp.{User, Item, ItemType} + alias Alog.Repo + + def seed_data() do + {:ok, item_type} = ItemType.insert(%{type: "Weapon"}) + + {:ok, item} = Item.insert(%{name: "Mjolnir"}) + {:ok, item_2} = Item.insert(%{name: "Staff"}) + + {:ok, item} = add_type_to_item(item, item_type) + {:ok, _item_2} = add_type_to_item(item_2, item_type) + + {:ok, user} = User.insert(%{name: "Thor", username: "gdofthndr12", postcode: "E2 0SY"}) + + {:ok, user} = add_item_to_user(user, item) + + {:ok, user, item} + end + + def add_type_to_item(item, type) do + item + |> Item.preload([:item_types, :user]) + |> Map.put(:id, nil) + |> Map.put(:inserted_at, nil) + |> Map.put(:updated_at, nil) + |> Item.changeset(%{}) + |> Ecto.Changeset.put_assoc(:item_types, [type]) + |> Repo.insert() + end + + def add_item_to_user(user, item) do + user + |> User.preload([:items]) + |> Map.put(:id, nil) + |> Map.put(:inserted_at, nil) + |> Map.put(:updated_at, nil) + |> User.changeset(%{}) + |> Ecto.Changeset.put_assoc(:items, [item]) + |> Repo.insert() + end +end diff --git a/test/support/item.ex b/test/support/item.ex new file mode 100644 index 0000000..69522d9 --- /dev/null +++ b/test/support/item.ex @@ -0,0 +1,29 @@ +defmodule Alog.TestApp.Item do + use Ecto.Schema + use Alog + import Ecto.Changeset + + schema "items" do + field(:name, :string) + field(:entry_id, :string) + field(:deleted, :boolean, default: false) + + belongs_to(:user, Alog.TestApp.User, foreign_key: :owner) + + many_to_many( + :item_types, + Alog.TestApp.ItemType, + join_through: "items_item_types", + join_keys: [item_id: :id, item_type_id: :id] + ) + + timestamps() + end + + @doc false + def changeset(address, attrs) do + address + |> cast(attrs, [:name]) + |> validate_required([:name]) + end +end diff --git a/test/support/item_type.ex b/test/support/item_type.ex new file mode 100644 index 0000000..31df3ed --- /dev/null +++ b/test/support/item_type.ex @@ -0,0 +1,27 @@ +defmodule Alog.TestApp.ItemType do + use Ecto.Schema + use Alog + import Ecto.Changeset + + schema "item_types" do + field(:type, :string) + field(:entry_id, :string) + field(:deleted, :boolean, default: false) + + many_to_many( + :items, + Alog.TestApp.Item, + join_through: "items_item_types", + join_keys: [item_type_id: :id, item_id: :id] + ) + + timestamps() + end + + @doc false + def changeset(address, attrs) do + address + |> cast(attrs, [:type]) + |> validate_required([:type]) + end +end diff --git a/test/support/user.ex b/test/support/user.ex index 3a6f9f1..0e088f7 100644 --- a/test/support/user.ex +++ b/test/support/user.ex @@ -10,6 +10,8 @@ defmodule Alog.TestApp.User do field(:entry_id, :string) field(:deleted, :boolean, default: false) + has_many(:items, Alog.TestApp.Item, foreign_key: :owner) + timestamps() end From 51b34e099f96719290a541b9ccd7725f1f8643ca Mon Sep 17 00:00:00 2001 From: Danielwhyte Date: Tue, 30 Oct 2018 12:58:24 +0000 Subject: [PATCH 2/2] adds get_by functionality --- lib/alog.ex | 21 +++++++++++++++++++-- test/alog_test.exs | 16 ++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/lib/alog.ex b/lib/alog.ex index 51065b9..eacb45b 100644 --- a/lib/alog.ex +++ b/lib/alog.ex @@ -51,7 +51,8 @@ defmodule Alog do defmacro __before_compile__(_env) do quote generated: true, location: :keep do - import Ecto.Query, only: [from: 2, subquery: 1] + import Ecto.Query + import Ecto.Query.API, only: [field: 2] @repo __MODULE__ |> Module.split() |> List.first() |> Module.concat("Repo") @@ -113,9 +114,25 @@ defmodule Alog do @doc """ Gets an item from the database that matches the given clause. + + User.get_by(username: "admin") + User.get_by(first_name: "Charlie", age: 27) """ def get_by(clauses) do - @repo.get_by(__MODULE__, clauses) + sub = + __MODULE__ + |> (fn q -> + Enum.reduce(clauses, q, fn {key, value}, q -> + q |> where([m], field(m, ^key) == ^value) + end) + end).() + |> order_by([m], desc: m.inserted_at) + |> limit([m], 1) + |> select([m], m) + + query = from(m in subquery(sub), where: not m.deleted, select: m) + + item = @repo.one(query) end @doc """ diff --git a/test/alog_test.exs b/test/alog_test.exs index 8c017b5..3c1a58f 100644 --- a/test/alog_test.exs +++ b/test/alog_test.exs @@ -84,6 +84,22 @@ defmodule AlogTest do 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 ->