From f0137ec1d91db45902ffd2e0f40de579dd423b4e Mon Sep 17 00:00:00 2001 From: Danielwhyte Date: Wed, 31 Oct 2018 15:19:27 +0000 Subject: [PATCH 1/4] updates guide for getting updated items --- .gitignore | 4 +- README.md | 111 ++++++++++++++++++++++++++++++++++ lib/append/append_only_log.ex | 25 ++++++-- mix.lock | 22 +++---- test/append/address_test.exs | 23 ++++--- 5 files changed, 160 insertions(+), 25 deletions(-) diff --git a/.gitignore b/.gitignore index 9779ca6..c2956f5 100644 --- a/.gitignore +++ b/.gitignore @@ -27,4 +27,6 @@ npm-debug.log # Alternatively, you may comment the line below and commit the # secrets files as long as you replace their contents by environment # variables. -/config/*.secret.exs \ No newline at end of file +/config/*.secret.exs + +.elixir_ls \ No newline at end of file diff --git a/README.md b/README.md index 09221e4..63230d0 100644 --- a/README.md +++ b/README.md @@ -919,6 +919,8 @@ We need to "clear" the `:id` field _before_ attempting to update (insert): def update(%__MODULE__{} = item, attrs) do item |> Map.put(:id, nil) + |> Map.put(:inserted_at, nil) + |> Map.put(:updated_at, nil) |> __MODULE__.changeset(attrs) |> Repo.insert() end @@ -927,6 +929,115 @@ end So here we remove the original autogenerated id from the existing item, preventing us from duplicating it in the database. +We also remove the `:inserted_at` and `:updated_at` fields. Again, if we leave those in, they'll be copied over from the old item, instead of being newly generated. + +Now we'll add another test, making sure our code so far is working as we expect it to: + +``` elixir +defmodule Append.AddressTest do + ... + test "get updated item" do + {:ok, item} = insert_address() + + {:ok, updated_item} = Address.update(item, %{tel: "0123444444"}) + + assert Address.get(item.id) == updated_item + end + ... +end +``` + +Here we're testing that the item we receive from our 'get' function is the new, updated item. + +Run this test and... + +``` elixir +1) test get updated item (Append.AddressTest) + test/append/address_test.exs:34 + Assertion with == failed + code: assert Address.get(item.id()) == updated_item + left: %Append.Address{... tel: "0800123123"} + right: %Append.Address{... tel: "0123444444"} + stacktrace: + test/append/address_test.exs:39: (test) +``` + +We're still getting the old item. + +To fix this we'll have to revisit our `get` function. + +``` elixir +def get(id) do + Repo.get(__MODULE__, id) +end +``` + +The first issue is that we're still using the `id` to get the item. As we know, this `id` will always reference the same `version` of the item, meaning no matter how many times we update it, the `id` will still point to the original, unmodified item. + +Luckily, we have another way to reference the item. Our `entry_id` that we created earlier. Let's use that in our query: + +``` +def get(entry_id) do + query = + from( + m in __MODULE__, + where: m.entry_id == ^entry_id, + select: m + ) + + item = Repo.one(query) +end +``` + +Don't forget to update the tests too: + +``` elixir +... +test "get/1" do + {:ok, item} = insert_address() + + assert Address.get(item.entry_id) == item +end +... +test "get updated item" do + {:ok, item} = insert_address() + + {:ok, updated_item} = Address.update(item, %{tel: "0123444444"}) + + assert Address.get(item.entry_id) == updated_item +end +... +``` + +Then we'll run the tests: + +``` elixir +test get updated item (Append.AddressTest) + test/append/address_test.exs:34 + ** (Ecto.MultipleResultsError) expected at most one result but got 2 in query: +``` + +Another error: "expected at most one result but got 2 in query" + +Of course, this makes sense, we have two items with that entry id, but we only want one. The most recent one. Let's modify our query further: + +``` elixir +def get(entry_id) do + query = + from( + m in __MODULE__, + where: m.entry_id == ^entry_id, + order_by: [desc: :inserted_at], + limit: 1, + select: m + ) + + item = Repo.one(query) +end +``` + +This will order our items in descending order by the inserted date, and take the most recent one. + #### 4.4 Get history diff --git a/lib/append/append_only_log.ex b/lib/append/append_only_log.ex index 29e4cff..9889d03 100644 --- a/lib/append/append_only_log.ex +++ b/lib/append/append_only_log.ex @@ -8,7 +8,7 @@ defmodule Append.AppendOnlyLog do @callback insert(struct) :: {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()} @callback get(integer) :: Ecto.Schema.t() | nil | no_return() - @callback get_by(Keyword.t() | map) :: Ecto.Schema.t() | nil | no_return() + @callback get_by(Keyword.t() | map) :: Ecto.Schema.t() | nil | no_return() @callback update(Ecto.Schema.t(), struct) :: {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()} @callback get_history(Ecto.Schema.t()) :: [Ecto.Schema.t()] | no_return() @@ -32,8 +32,17 @@ defmodule Append.AppendOnlyLog do |> Repo.insert() end - def get(id) do - Repo.get(__MODULE__, id) + def get(entry_id) do + query = + from( + m in __MODULE__, + where: m.entry_id == ^entry_id, + order_by: [desc: :inserted_at], + limit: 1, + select: m + ) + + item = Repo.one(query) end def get_by(clauses) do @@ -43,14 +52,18 @@ defmodule Append.AppendOnlyLog do def update(%__MODULE__{} = item, attrs) do item |> Map.put(:id, nil) + |> Map.put(:inserted_at, nil) + |> Map.put(:updated_at, nil) |> __MODULE__.changeset(attrs) |> Repo.insert() end def get_history(%__MODULE__{} = item) do - query = from m in __MODULE__, - where: m.entry_id == ^item.entry_id, - select: m + query = + from(m in __MODULE__, + where: m.entry_id == ^item.entry_id, + select: m + ) Repo.all(query) end diff --git a/mix.lock b/mix.lock index 35fc4d5..5385d33 100644 --- a/mix.lock +++ b/mix.lock @@ -1,22 +1,22 @@ %{ - "certifi": {:hex, :certifi, "2.4.2", "75424ff0f3baaccfd34b1214184b6ef616d89e420b258bb0a5ea7d7bc628f7f0", [], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"}, + "certifi": {:hex, :certifi, "2.4.2", "75424ff0f3baaccfd34b1214184b6ef616d89e420b258bb0a5ea7d7bc628f7f0", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"}, "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm"}, "cowboy": {:hex, :cowboy, "1.1.2", "61ac29ea970389a88eca5a65601460162d370a70018afe6f949a29dca91f3bb0", [:rebar3], [{:cowlib, "~> 1.0.2", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3.2", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm"}, "cowlib": {:hex, :cowlib, "1.0.2", "9d769a1d062c9c3ac753096f868ca121e2730b9a377de23dec0f7e08b1df84ee", [:make], [], "hexpm"}, "db_connection": {:hex, :db_connection, "1.1.3", "89b30ca1ef0a3b469b1c779579590688561d586694a3ce8792985d4d7e575a61", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm"}, "decimal": {:hex, :decimal, "1.5.0", "b0433a36d0e2430e3d50291b1c65f53c37d56f83665b43d79963684865beab68", [:mix], [], "hexpm"}, "ecto": {:hex, :ecto, "2.2.10", "e7366dc82f48f8dd78fcbf3ab50985ceeb11cb3dc93435147c6e13f2cda0992e", [:mix], [{:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: true]}, {:decimal, "~> 1.2", [hex: :decimal, repo: "hexpm", optional: false]}, {:mariaex, "~> 0.8.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.13.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm"}, - "excoveralls": {:hex, :excoveralls, "0.7.5", "339e433e5d3bce09400dc8de7b9040741a409c93917849916c136a0f51fdc183", [], [{:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: false]}, {:hackney, ">= 0.12.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, - "exjsx": {:hex, :exjsx, "4.0.0", "60548841e0212df401e38e63c0078ec57b33e7ea49b032c796ccad8cde794b5c", [], [{:jsx, "~> 2.8.0", [hex: :jsx, repo: "hexpm", optional: false]}], "hexpm"}, + "excoveralls": {:hex, :excoveralls, "0.7.5", "339e433e5d3bce09400dc8de7b9040741a409c93917849916c136a0f51fdc183", [:mix], [{:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: false]}, {:hackney, ">= 0.12.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, + "exjsx": {:hex, :exjsx, "4.0.0", "60548841e0212df401e38e63c0078ec57b33e7ea49b032c796ccad8cde794b5c", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, repo: "hexpm", optional: false]}], "hexpm"}, "file_system": {:hex, :file_system, "0.2.6", "fd4dc3af89b9ab1dc8ccbcc214a0e60c41f34be251d9307920748a14bf41f1d3", [:mix], [], "hexpm"}, "gettext": {:hex, :gettext, "0.16.0", "4a7e90408cef5f1bf57c5a39e2db8c372a906031cc9b1466e963101cb927dafc", [:mix], [], "hexpm"}, - "hackney": {:hex, :hackney, "1.14.0", "66e29e78feba52176c3a4213d42b29bdc4baff93a18cfe480f73b04677139dee", [], [{:certifi, "2.4.2", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.4", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, - "idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"}, - "jsx": {:hex, :jsx, "2.8.3", "a05252d381885240744d955fbe3cf810504eb2567164824e19303ea59eef62cf", [], [], "hexpm"}, - "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [], [], "hexpm"}, + "hackney": {:hex, :hackney, "1.14.0", "66e29e78feba52176c3a4213d42b29bdc4baff93a18cfe480f73b04677139dee", [:rebar3], [{:certifi, "2.4.2", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.4", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, + "idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"}, + "jsx": {:hex, :jsx, "2.8.3", "a05252d381885240744d955fbe3cf810504eb2567164824e19303ea59eef62cf", [:mix, :rebar3], [], "hexpm"}, + "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"}, "mime": {:hex, :mime, "1.3.0", "5e8d45a39e95c650900d03f897fbf99ae04f60ab1daa4a34c7a20a5151b7a5fe", [:mix], [], "hexpm"}, - "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [], [], "hexpm"}, - "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [], [], "hexpm"}, + "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], [], "hexpm"}, + "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm"}, "phoenix": {:hex, :phoenix, "1.3.4", "aaa1b55e5523083a877bcbe9886d9ee180bf2c8754905323493c2ac325903dc5", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.3.3 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, "phoenix_ecto": {:hex, :phoenix_ecto, "3.4.0", "91cd39427006fe4b5588d69f0941b9c3d3d8f5e6477c563a08379de7de2b0c58", [:mix], [{:ecto, "~> 2.1", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.9", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, "phoenix_html": {:hex, :phoenix_html, "2.12.0", "1fb3c2e48b4b66d75564d8d63df6d53655469216d6b553e7e14ced2b46f97622", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, @@ -27,6 +27,6 @@ "poolboy": {:hex, :poolboy, "1.5.1", "6b46163901cfd0a1b43d692657ed9d7e599853b3b21b95ae5ae0a777cf9b6ca8", [:rebar], [], "hexpm"}, "postgrex": {:hex, :postgrex, "0.13.5", "3d931aba29363e1443da167a4b12f06dcd171103c424de15e5f3fc2ba3e6d9c5", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm"}, "ranch": {:hex, :ranch, "1.3.2", "e4965a144dc9fbe70e5c077c65e73c57165416a901bd02ea899cfd95aa890986", [:rebar3], [], "hexpm"}, - "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.4", "f0eafff810d2041e93f915ef59899c923f4568f4585904d010387ed74988e77b", [], [], "hexpm"}, - "unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [], [], "hexpm"}, + "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.4", "f0eafff810d2041e93f915ef59899c923f4568f4585904d010387ed74988e77b", [:make, :mix, :rebar3], [], "hexpm"}, + "unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm"}, } diff --git a/test/append/address_test.exs b/test/append/address_test.exs index 4c0122b..79411e6 100644 --- a/test/append/address_test.exs +++ b/test/append/address_test.exs @@ -12,7 +12,7 @@ defmodule Append.AddressTest do test "get/1" do {:ok, item} = insert_address() - assert Address.get(item.id) == item + assert Address.get(item.entry_id) == item end test "get_by/1" do @@ -31,15 +31,24 @@ defmodule Append.AddressTest do assert updated_item.tel != item.tel end + test "get updated item" do + {:ok, item} = insert_address() + + {:ok, updated_item} = Address.update(item, %{tel: "0123444444"}) + + assert Address.get(item.entry_id) == updated_item + end + test "get history of item" do {:ok, item} = insert_address() - {:ok, updated_item} = Address.update(item, %{ - address_line_1: "12", - address_line_2: "Kvadraturen", - city: "Oslo", - postcode: "NW SCA", - }) + {:ok, updated_item} = + Address.update(item, %{ + address_line_1: "12", + address_line_2: "Kvadraturen", + city: "Oslo", + postcode: "NW SCA" + }) history = Address.get_history(updated_item) From f5aa1a7faa8d0ee48b486c906533a7c903fdea4c Mon Sep 17 00:00:00 2001 From: Danielwhyte Date: Wed, 31 Oct 2018 15:26:46 +0000 Subject: [PATCH 2/4] removes get_by function --- README.md | 23 ++++------------------- lib/append/append_only_log.ex | 5 ----- test/append/address_test.exs | 6 ------ 3 files changed, 4 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 63230d0..c1d3c55 100644 --- a/README.md +++ b/README.md @@ -425,7 +425,6 @@ defmodule Append.AppendOnlyLog do @callback insert @callback get - @callback get_by @callback update defmacro __using__(_opts) do @@ -434,7 +433,7 @@ defmodule Append.AppendOnlyLog do end ``` -These are the four functions we'll define in this macro to interface with the database. +These are the functions we'll define in this macro to interface with the database. You may think it odd that we're defining an `update` function for our append-only database, but we'll get to that later. @@ -448,7 +447,6 @@ defmodule Append.AppendOnlyLog do @callback insert(struct) :: {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()} @callback get(integer) :: Ecto.Schema.t() | nil | no_return() - @callback get_by(Keyword.t() | map) :: Ecto.Schema.t() | nil | no_return() @callback update(Ecto.Schema.t(), struct) :: {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()} defmacro __using__(_opts) do @@ -461,9 +459,6 @@ defmodule Append.AppendOnlyLog do def get(id) do end - def get_by(clauses) do - end - def update(item, attrs) do end end @@ -678,13 +673,13 @@ Finished in 0.1 seconds 4 tests, 0 failures ``` -#### 4.2 Get/Get By +#### 4.2 Get Now that we've done the _hard parts_, we'll implement the rest of the functionality for our Append Only Log. -The `get` and `get by` functions should be fairly simple, we just need to forward -the requests to the Repo. But first, a test. +The `get` function should be fairly simple, we just need to forward +the request to the Repo. But first, a test. ``` elixir defmodule Append.AddressTest do @@ -695,12 +690,6 @@ defmodule Append.AddressTest do assert Address.get(item.id) == item end - - test "get_by/1" do - {:ok, item} = insert_address() - - assert Address.get_by(name: "Thor") == item - end end def insert_address do @@ -730,10 +719,6 @@ defmodule Append.AppendOnlyLog do def get(id) do Repo.get(__MODULE__, id) end - - def get_by(clauses) do - Repo.get_by(__MODULE__, clauses) - end ... end end diff --git a/lib/append/append_only_log.ex b/lib/append/append_only_log.ex index 9889d03..e6bbcc9 100644 --- a/lib/append/append_only_log.ex +++ b/lib/append/append_only_log.ex @@ -8,7 +8,6 @@ defmodule Append.AppendOnlyLog do @callback insert(struct) :: {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()} @callback get(integer) :: Ecto.Schema.t() | nil | no_return() - @callback get_by(Keyword.t() | map) :: Ecto.Schema.t() | nil | no_return() @callback update(Ecto.Schema.t(), struct) :: {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()} @callback get_history(Ecto.Schema.t()) :: [Ecto.Schema.t()] | no_return() @@ -45,10 +44,6 @@ defmodule Append.AppendOnlyLog do item = Repo.one(query) end - def get_by(clauses) do - Repo.get_by(__MODULE__, clauses) - end - def update(%__MODULE__{} = item, attrs) do item |> Map.put(:id, nil) diff --git a/test/append/address_test.exs b/test/append/address_test.exs index 79411e6..ff68dad 100644 --- a/test/append/address_test.exs +++ b/test/append/address_test.exs @@ -14,12 +14,6 @@ defmodule Append.AddressTest do assert Address.get(item.entry_id) == item end - - test "get_by/1" do - {:ok, item} = insert_address() - - assert Address.get_by(name: "Thor") == item - end end test "update item in database" do From 214d30d3b7b65b9e24403f79e4b9cc55dc1f315e Mon Sep 17 00:00:00 2001 From: Danielwhyte Date: Wed, 31 Oct 2018 16:28:03 +0000 Subject: [PATCH 3/4] adds delete functionality to guide --- README.md | 108 +++++++++++++++++- lib/append/address.ex | 22 +++- lib/append/append_only_log.ex | 15 ++- .../migrations/20181031160106_add_deleted.exs | 9 ++ test/append/address_test.exs | 9 ++ 5 files changed, 156 insertions(+), 7 deletions(-) create mode 100644 priv/repo/migrations/20181031160106_add_deleted.exs diff --git a/README.md b/README.md index c1d3c55..d615fb6 100644 --- a/README.md +++ b/README.md @@ -426,6 +426,7 @@ defmodule Append.AppendOnlyLog do @callback insert @callback get @callback update + @callback delete defmacro __using__(_opts) do ... @@ -448,6 +449,7 @@ defmodule Append.AppendOnlyLog do @callback insert(struct) :: {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()} @callback get(integer) :: Ecto.Schema.t() | nil | no_return() @callback update(Ecto.Schema.t(), struct) :: {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()} + @callback delete(Ecto.Schema.t()) :: {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()} defmacro __using__(_opts) do quote do @@ -961,7 +963,7 @@ The first issue is that we're still using the `id` to get the item. As we know, Luckily, we have another way to reference the item. Our `entry_id` that we created earlier. Let's use that in our query: -``` +``` elixir def get(entry_id) do query = from( @@ -1026,7 +1028,7 @@ This will order our items in descending order by the inserted date, and take the #### 4.4 Get history -The final part of our append-only database will be the functionality +A useful part of our append-only database will be the functionality to see the entire history of an item. As usual, we'll write a test first: @@ -1068,7 +1070,7 @@ defmodule Append.AppendOnlyLog do ... defmacro __before_compile__(_env) do quote do - import Ecto.Query, only: [from: 2] + import Ecto.Query ... def get_history(%__MODULE__{} = item) do @@ -1092,3 +1094,103 @@ where we end up calling it. Now run your tests, and you'll see that we're now able to view the whole history of the changes of all items in our database. + +#### 4.5 Delete + +As you may realise, even though we are using an append only database, we still need some way to "delete" items. + +Of course they won't actually be deleted, merely marked as "inactive", so they don't show anywhere unless we specifically want them to (For example in our history function). + +To implement this functionality, we'll need to add a field to our schema, and to the cast function in our changeset. + +``` elixir +defmodule Append.Address do + schema "addresses" do + ... + field(:deleted, :boolean, default: false) + ... + end + + def changeset(address, attrs) do + address + |> insert_entry_id() + |> cast(attrs, [ + ..., + :deleted + ]) + |> validate_required([ + ... + ]) + end +end +``` + +and a new migration: +``` +mix ecto.gen.migration add_deleted +``` + +``` elixir +defmodule Append.Repo.Migrations.AddDeleted do + use Ecto.Migration + + def change do + alter table("addresses") do + add(:deleted, :boolean, default: false) + end + end +end +``` + +This adds a `boolean` field, with a default value of `false`. We'll use this to determine whether a given item is "deleted" or not. + +As usual, before we implement it, we'll add a test for our expected functionality. + +``` elixir +describe "delete:" do + test "deleted items are not retrieved with 'get'" do + {:ok, item} = insert_address() + {:ok, _} = Address.delete(item) + + assert Address.get(item.entry_id) == nil + end + end +``` + +Our delete function is fairly simple: + +``` elixir +def delete(%__MODULE__{} = item) do + item + |> Map.put(:id, nil) + |> Map.put(:inserted_at, nil) + |> Map.put(:updated_at, nil) + |> __MODULE__.changeset(%{deleted: true}) + |> Repo.insert() +end +``` + +It acts just the same as the update function, but adds a value of `deleted: true`. But this is only half of the story. + +We also need to make sure we don't return any deleted items when they're requested. So again, we'll have to edit our `get` function: + +``` elixir +def get(entry_id) do + sub = + from( + m in __MODULE__, + where: m.entry_id == ^entry_id, + order_by: [desc: :inserted_at], + limit: 1, + select: m + ) + + query = from(m in subquery(sub), where: not m.deleted, select: m) + + item = Repo.one(query) +end +``` + +What we're doing here is taking our original query, then performing another query on the result of that, only returning the item if it has not been marked as `deleted`. + +So now, when we run our tests, we should see that we're succesfully ignoring "deleted" items. \ No newline at end of file diff --git a/lib/append/address.ex b/lib/append/address.ex index d447d4a..47b28be 100644 --- a/lib/append/address.ex +++ b/lib/append/address.ex @@ -11,6 +11,7 @@ defmodule Append.Address do field(:postcode, :string) field(:tel, :string) field(:entry_id, :string) + field(:deleted, :boolean, default: false) timestamps() end @@ -19,8 +20,25 @@ defmodule Append.Address do def changeset(address, attrs) do address |> insert_entry_id() - |> cast(attrs, [:name, :address_line_1, :address_line_2, :city, :postcode, :tel, :entry_id]) - |> validate_required([:name, :address_line_1, :address_line_2, :city, :postcode, :tel, :entry_id]) + |> cast(attrs, [ + :name, + :address_line_1, + :address_line_2, + :city, + :postcode, + :tel, + :entry_id, + :deleted + ]) + |> validate_required([ + :name, + :address_line_1, + :address_line_2, + :city, + :postcode, + :tel, + :entry_id + ]) end def insert_entry_id(address) do diff --git a/lib/append/append_only_log.ex b/lib/append/append_only_log.ex index e6bbcc9..4e9ec52 100644 --- a/lib/append/append_only_log.ex +++ b/lib/append/append_only_log.ex @@ -23,7 +23,7 @@ defmodule Append.AppendOnlyLog do defmacro __before_compile__(_env) do quote do - import Ecto.Query, only: [from: 2] + import Ecto.Query def insert(attrs) do %__MODULE__{} @@ -32,7 +32,7 @@ defmodule Append.AppendOnlyLog do end def get(entry_id) do - query = + sub = from( m in __MODULE__, where: m.entry_id == ^entry_id, @@ -41,6 +41,8 @@ defmodule Append.AppendOnlyLog do select: m ) + query = from(m in subquery(sub), where: not m.deleted, select: m) + item = Repo.one(query) end @@ -62,6 +64,15 @@ defmodule Append.AppendOnlyLog do Repo.all(query) end + + def delete(%__MODULE__{} = item) do + item + |> Map.put(:id, nil) + |> Map.put(:inserted_at, nil) + |> Map.put(:updated_at, nil) + |> __MODULE__.changeset(%{deleted: true}) + |> Repo.insert() + end end end end diff --git a/priv/repo/migrations/20181031160106_add_deleted.exs b/priv/repo/migrations/20181031160106_add_deleted.exs new file mode 100644 index 0000000..e6656cb --- /dev/null +++ b/priv/repo/migrations/20181031160106_add_deleted.exs @@ -0,0 +1,9 @@ +defmodule Append.Repo.Migrations.AddDeleted do + use Ecto.Migration + + def change do + alter table("addresses") do + add(:deleted, :boolean, default: false) + end + end +end diff --git a/test/append/address_test.exs b/test/append/address_test.exs index ff68dad..456e00f 100644 --- a/test/append/address_test.exs +++ b/test/append/address_test.exs @@ -62,4 +62,13 @@ defmodule Append.AddressTest do tel: "0800123123" }) end + + describe "delete:" do + test "deleted items are not retrieved with 'get'" do + {:ok, item} = insert_address() + {:ok, _} = Address.delete(item) + + assert Address.get(item.entry_id) == nil + end + end end From e76b8ea8338e5e9d7d987dbf2c599bc81a924669 Mon Sep 17 00:00:00 2001 From: Danielwhyte Date: Wed, 31 Oct 2018 16:53:06 +0000 Subject: [PATCH 4/4] adds all function to guide --- README.md | 99 ++++++++++++++++++++++++++++++----- lib/append/append_only_log.ex | 16 +++++- test/append/address_test.exs | 28 ++++++++-- 3 files changed, 126 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index d615fb6..2a34f39 100644 --- a/README.md +++ b/README.md @@ -425,6 +425,7 @@ defmodule Append.AppendOnlyLog do @callback insert @callback get + @callback all @callback update @callback delete @@ -448,6 +449,7 @@ defmodule Append.AppendOnlyLog do @callback insert(struct) :: {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()} @callback get(integer) :: Ecto.Schema.t() | nil | no_return() + @callback all() :: [Ecto.Schema.t()] @callback update(Ecto.Schema.t(), struct) :: {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()} @callback delete(Ecto.Schema.t()) :: {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()} @@ -461,6 +463,9 @@ defmodule Append.AppendOnlyLog do def get(id) do end + def all() do + end + def update(item, attrs) do end end @@ -655,6 +660,9 @@ defmodule Append.AppendOnlyLog do def get(id) do end + def all() do + end + def update(item, attrs) do end end @@ -675,28 +683,35 @@ Finished in 0.1 seconds 4 tests, 0 failures ``` -#### 4.2 Get +#### 4.2 Get/All Now that we've done the _hard parts_, we'll implement the rest of the functionality for our Append Only Log. -The `get` function should be fairly simple, we just need to forward -the request to the Repo. But first, a test. +The `get` and `all` functions should be fairly simple, we just need to forward +the requests to the Repo. But first, some tests. ``` elixir defmodule Append.AddressTest do ... - describe "get item from database" do + describe "get items from database" do test "get/1" do {:ok, item} = insert_address() assert Address.get(item.id) == item end + + test "all/0" do + {:ok, item} = insert_address() + {:ok, item_2} = insert_address("Loki") + + assert length(Address.all()) == 2 + end end - def insert_address do + def insert_address(name \\ "Thor") do Address.insert(%{ - name: "Thor", + name: name, address_line_1: "The Hall", address_line_2: "Valhalla", city: "Asgard", @@ -721,6 +736,10 @@ defmodule Append.AppendOnlyLog do def get(id) do Repo.get(__MODULE__, id) end + + def all do + Repo.all(__MODULE__) + end ... end end @@ -918,7 +937,7 @@ preventing us from duplicating it in the database. We also remove the `:inserted_at` and `:updated_at` fields. Again, if we leave those in, they'll be copied over from the old item, instead of being newly generated. -Now we'll add another test, making sure our code so far is working as we expect it to: +Now we'll add some more tests, making sure our code so far is working as we expect it to: ``` elixir defmodule Append.AddressTest do @@ -930,11 +949,19 @@ defmodule Append.AddressTest do assert Address.get(item.id) == updated_item end + + test "all/0 does not include old items" do + {:ok, item} = insert_address() + {:ok, _} = insert_address("Loki") + {:ok, _} = Address.update(item, %{postcode: "W2 3EC"}) + + assert length(Address.all()) == 2 + end ... end ``` -Here we're testing that the item we receive from our 'get' function is the new, updated item. +Here we're testing that the items we receive from our 'get' and 'all' functions are the new, updated items. Run this test and... @@ -947,9 +974,18 @@ Run this test and... right: %Append.Address{... tel: "0123444444"} stacktrace: test/append/address_test.exs:39: (test) + +2) test all/0 does not include old items (Append.AddressTest) + test/append/address_test.exs:43 + Assertion with == failed + code: assert length(Address.all()) == 2 + left: 3 + right: 2 + stacktrace: + test/append/address_test.exs:48: (test) ``` -We're still getting the old item. +We're still getting the old items. To fix this we'll have to revisit our `get` function. @@ -972,7 +1008,7 @@ def get(entry_id) do select: m ) - item = Repo.one(query) + Repo.one(query) end ``` @@ -1019,12 +1055,29 @@ def get(entry_id) do select: m ) - item = Repo.one(query) + Repo.one(query) end ``` This will order our items in descending order by the inserted date, and take the most recent one. +We'll use the same query in our `all` function, but replacing the `limit: 1` with `distinct: entry_id`: + +``` elixir +def all do + sub = + from(m in __MODULE__, + distinct: m.entry_id, + order_by: [desc: :inserted_at], + select: m + ) + + Repo.all(query) +end +``` + +This ensures we get more than one item, but only the most recent of each `entry_id`. + #### 4.4 Get history @@ -1154,6 +1207,13 @@ describe "delete:" do assert Address.get(item.entry_id) == nil end + + test "deleted items are not retrieved with 'all'" do + {:ok, item} = insert_address() + {:ok, _} = Address.delete(item) + + assert length(Address.all()) == 0 + end end ``` @@ -1172,7 +1232,7 @@ end It acts just the same as the update function, but adds a value of `deleted: true`. But this is only half of the story. -We also need to make sure we don't return any deleted items when they're requested. So again, we'll have to edit our `get` function: +We also need to make sure we don't return any deleted items when they're requested. So again, we'll have to edit our `get` and `all` functions: ``` elixir def get(entry_id) do @@ -1187,7 +1247,20 @@ def get(entry_id) do query = from(m in subquery(sub), where: not m.deleted, select: m) - item = Repo.one(query) + Repo.one(query) +end + +def all do + sub = + from(m in __MODULE__, + distinct: m.entry_id, + order_by: [desc: :inserted_at], + select: m + ) + + query = from(m in subquery(sub), where: not m.deleted, select: m) + + Repo.all(query) end ``` diff --git a/lib/append/append_only_log.ex b/lib/append/append_only_log.ex index 4e9ec52..b8e059d 100644 --- a/lib/append/append_only_log.ex +++ b/lib/append/append_only_log.ex @@ -8,6 +8,7 @@ defmodule Append.AppendOnlyLog do @callback insert(struct) :: {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()} @callback get(integer) :: Ecto.Schema.t() | nil | no_return() + @callback all() :: [Ecto.Schema.t()] @callback update(Ecto.Schema.t(), struct) :: {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()} @callback get_history(Ecto.Schema.t()) :: [Ecto.Schema.t()] | no_return() @@ -43,7 +44,20 @@ defmodule Append.AppendOnlyLog do query = from(m in subquery(sub), where: not m.deleted, select: m) - item = Repo.one(query) + Repo.one(query) + end + + def all do + sub = + from(m in __MODULE__, + distinct: m.entry_id, + order_by: [desc: :inserted_at], + select: m + ) + + query = from(m in subquery(sub), where: not m.deleted, select: m) + + Repo.all(query) end def update(%__MODULE__{} = item, attrs) do diff --git a/test/append/address_test.exs b/test/append/address_test.exs index 456e00f..7dc4181 100644 --- a/test/append/address_test.exs +++ b/test/append/address_test.exs @@ -8,12 +8,19 @@ defmodule Append.AddressTest do assert item.name == "Thor" end - describe "get item from database" do + describe "get items from database" do test "get/1" do {:ok, item} = insert_address() assert Address.get(item.entry_id) == item end + + test "all/0" do + {:ok, _} = insert_address() + {:ok, _} = insert_address("Loki") + + assert length(Address.all()) == 2 + end end test "update item in database" do @@ -33,6 +40,14 @@ defmodule Append.AddressTest do assert Address.get(item.entry_id) == updated_item end + test "all/0 does not include old items" do + {:ok, item} = insert_address() + {:ok, _} = insert_address("Loki") + {:ok, _} = Address.update(item, %{postcode: "W2 3EC"}) + + assert length(Address.all()) == 2 + end + test "get history of item" do {:ok, item} = insert_address() @@ -52,9 +67,9 @@ defmodule Append.AddressTest do assert Map.fetch(h2, :city) == {:ok, "Oslo"} end - def insert_address do + def insert_address(name \\ "Thor") do Address.insert(%{ - name: "Thor", + name: name, address_line_1: "The Hall", address_line_2: "Valhalla", city: "Asgard", @@ -70,5 +85,12 @@ defmodule Append.AddressTest do assert Address.get(item.entry_id) == nil end + + test "deleted items are not retrieved with 'all'" do + {:ok, item} = insert_address() + {:ok, _} = Address.delete(item) + + assert length(Address.all()) == 0 + end end end