From 76d326460ada147082c079e330cc851b7592d429 Mon Sep 17 00:00:00 2001 From: Johnny Winn Date: Tue, 19 Sep 2017 20:29:00 -0600 Subject: [PATCH 1/4] Adjust message for unique constraint on author --- .../api_web/controllers/api/v1/fallback_controller.ex | 10 ++++++++++ apps/core/lib/core/schemas/author.ex | 3 +-- apps/core/test/core/schemas/author_test.exs | 2 +- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/apps/api/lib/api_web/controllers/api/v1/fallback_controller.ex b/apps/api/lib/api_web/controllers/api/v1/fallback_controller.ex index 2c31a81..2ed8b9c 100644 --- a/apps/api/lib/api_web/controllers/api/v1/fallback_controller.ex +++ b/apps/api/lib/api_web/controllers/api/v1/fallback_controller.ex @@ -21,6 +21,12 @@ defmodule ApiWeb.Api.V1.FallbackController do |> render_errors(%{status: "401", detail: "Unauthorized"}) end + def call(conn, {:error, %Ecto.Changeset{errors: errors}}) do + conn + |> put_status(:bad_request) + |> render_errors(%{status: "400", detail: format_changest_errors(errors)}) + end + def call(conn, {:error, reason}) do conn |> put_status(:bad_request) @@ -44,4 +50,8 @@ defmodule ApiWeb.Api.V1.FallbackController do |> render("errors.json-api", data: data) end + defp format_changest_errors(errors) do + Enum.map(errors, fn({key, {val, _}}) -> "#{key} #{val}" end) + end + end diff --git a/apps/core/lib/core/schemas/author.ex b/apps/core/lib/core/schemas/author.ex index 1fd902c..387c081 100644 --- a/apps/core/lib/core/schemas/author.ex +++ b/apps/core/lib/core/schemas/author.ex @@ -8,7 +8,6 @@ defmodule Core.Schemas.Author do """ @unique_index :index_unique_active_authors_idx - @unique_error "already exists" @primary_key {:id, :binary_id, autogenerate: true} @@ -29,6 +28,6 @@ defmodule Core.Schemas.Author do model |> cast(params, @required_fields ++ @optional_fields) |> validate_required(@required_fields) - |> unique_constraint(:author, name: @unique_index, message: @unique_error) + |> unique_constraint(:username, name: @unique_index) end end diff --git a/apps/core/test/core/schemas/author_test.exs b/apps/core/test/core/schemas/author_test.exs index 1af8eca..229353e 100644 --- a/apps/core/test/core/schemas/author_test.exs +++ b/apps/core/test/core/schemas/author_test.exs @@ -28,6 +28,6 @@ defmodule Core.Schemas.AuthorTest do |> Author.changeset(@valid_attrs) |> Repo.insert - assert changeset.errors == [author: {"already exists", []}] + assert changeset.errors == [username: {"has already been taken", []}] end end From e2c0d597bca19f4b23a379e13e0a3c29edeea0b1 Mon Sep 17 00:00:00 2001 From: Johnny Winn Date: Wed, 20 Sep 2017 00:25:17 -0600 Subject: [PATCH 2/4] Cleanup found in testing API --- apps/api/lib/api_web/views/dashboard_view.ex | 4 --- apps/core/lib/core/data/dashboard.ex | 4 ++- apps/core/lib/core/data/query.ex | 38 ++++++++++++++++---- apps/core/lib/core/data/widget.ex | 4 ++- apps/core/lib/core/schemas/dashboard.ex | 2 +- apps/core/lib/core/schemas/widget.ex | 2 +- apps/core/test/core/data/author_test.exs | 18 +++++++--- 7 files changed, 54 insertions(+), 18 deletions(-) diff --git a/apps/api/lib/api_web/views/dashboard_view.ex b/apps/api/lib/api_web/views/dashboard_view.ex index c9a0edc..6765015 100644 --- a/apps/api/lib/api_web/views/dashboard_view.ex +++ b/apps/api/lib/api_web/views/dashboard_view.ex @@ -13,8 +13,4 @@ defmodule ApiWeb.Api.V1.DashboardView do serializer: ApiWeb.Api.V1.AuthorView, include: true - has_many :widgets, - serializer: ApiWeb.Api.V1.WidgetView, - include: true - end diff --git a/apps/core/lib/core/data/dashboard.ex b/apps/core/lib/core/data/dashboard.ex index 54e15b5..b674cc2 100644 --- a/apps/core/lib/core/data/dashboard.ex +++ b/apps/core/lib/core/data/dashboard.ex @@ -1,3 +1,5 @@ defmodule Core.Data.Dashboard do - use Core.Data.Query, schema: Core.Schemas.Dashboard + use Core.Data.Query, + schema: Core.Schemas.Dashboard, + preloads: [:author] end diff --git a/apps/core/lib/core/data/query.ex b/apps/core/lib/core/data/query.ex index 530dd0e..2e09ad6 100644 --- a/apps/core/lib/core/data/query.ex +++ b/apps/core/lib/core/data/query.ex @@ -16,30 +16,43 @@ defmodule Core.Data.Query do defmacro __using__(opts \\ []) do schema = opts[:schema] || __CALLER__.module + preloads = opts[:preloads] || [] quote do alias Core.Repo alias unquote(schema) + import Ecto.Query, only: [from: 2] + @type t :: %unquote(schema){} @behaviour Core.Data.Query def all(repo \\ Repo) do - unquote(schema) + from(m in unquote(schema), where: m.deleted == false) |> repo.all() + |> repo.preload(unquote(preloads)) end def get(id, repo \\ Repo) def get(id, repo) when is_binary(id) do - repo.get_by(unquote(schema), %{id: id}) + try do + result = unquote(schema) + |> repo.get_by(%{id: id}) + |> repo.preload(unquote(preloads)) + + {:ok, result} + rescue + error -> + {:error, "Invalid #{inspect error.type} with value #{inspect error.value}"} + end end - def get(_id, _repo), do: nil + def get(_id, _repo), do: {:ok, nil} def upsert(params, repo \\ Repo) do - with %unquote(schema){} = model <- get(params["id"], repo), + with {:ok, %unquote(schema){} = model} <- get(params["id"], repo), {:ok, %unquote(schema){}} = result <- update(model, params, repo) do result else - nil -> + {:ok, nil} -> create(params, repo) {:error, %Ecto.Changeset{}} = changeset_error -> changeset_error @@ -53,13 +66,19 @@ defmodule Core.Data.Query do "id" => id, "deleted" => true, "deleted_at" => DateTime.utc_now() - } |> upsert(repo) + } + |> upsert(repo) + |> case do + {:ok, %unquote(schema){}} -> :ok + error -> error + end end defp update(%unquote(schema){} = author, params, repo) do author |> unquote(schema).changeset(params) |> repo.update() + |> preload(repo) end defoverridable [update: 3] @@ -67,9 +86,16 @@ defmodule Core.Data.Query do %unquote(schema){} |> unquote(schema).changeset(params) |> repo.insert() + |> preload(repo) end defoverridable [create: 2] + defp preload({:ok, model}, repo) do + {:ok, repo.preload(model, unquote(preloads))} + end + + defp preload(error, _repo), do: error + end end end diff --git a/apps/core/lib/core/data/widget.ex b/apps/core/lib/core/data/widget.ex index 5941b6d..868a686 100644 --- a/apps/core/lib/core/data/widget.ex +++ b/apps/core/lib/core/data/widget.ex @@ -1,3 +1,5 @@ defmodule Core.Data.Widget do - use Core.Data.Query, schema: Core.Schemas.Widget + use Core.Data.Query, + schema: Core.Schemas.Widget, + preloads: [:author, :data_sources] end diff --git a/apps/core/lib/core/schemas/dashboard.ex b/apps/core/lib/core/schemas/dashboard.ex index 7fd3574..3dbd37e 100644 --- a/apps/core/lib/core/schemas/dashboard.ex +++ b/apps/core/lib/core/schemas/dashboard.ex @@ -46,7 +46,7 @@ defmodule Core.Schemas.Dashboard do |> validate_required(@required_fields) |> foreign_key_constraint(:author_id) |> unique_constraint(:dashboard, name: @unique_index, message: @unique_error) - |> put_assoc(:dashboard_widgets, params["dashboard_widgets"]) + |> cast_assoc(:dashboard_widgets, params["dashboard_widgets"]) end def changeset(model, params) do diff --git a/apps/core/lib/core/schemas/widget.ex b/apps/core/lib/core/schemas/widget.ex index 3d1073d..5681dd5 100644 --- a/apps/core/lib/core/schemas/widget.ex +++ b/apps/core/lib/core/schemas/widget.ex @@ -59,7 +59,7 @@ defmodule Core.Schemas.Widget do |> validate_required(@required_fields) |> foreign_key_constraint(:author_id) |> unique_constraint(:widget, name: @unique_index, message: @unique_error) - |> put_assoc(:widget_data_sources, params["widget_data_sources"]) + |> cast_assoc(:widget_data_sources, data_sources) end def changeset(model, params) do diff --git a/apps/core/test/core/data/author_test.exs b/apps/core/test/core/data/author_test.exs index 6550007..5ee0815 100644 --- a/apps/core/test/core/data/author_test.exs +++ b/apps/core/test/core/data/author_test.exs @@ -7,6 +7,11 @@ defmodule Core.Data.AuthorTest do alias Core.Data.Author + test "get an author by id" do + assert {:ok, author} = Author.upsert(%{"username" => "bob-dobbs"}) + assert {:ok, author} == Author.get(author.id) + end + test "create a new author with valid params" do assert [] = Author.all() assert {:ok, author} = Author.upsert(%{"username" => "bob-dobbs"}) @@ -28,10 +33,15 @@ defmodule Core.Data.AuthorTest do {:ok, author} = Author.upsert(%{"username" => "bob-dobbs"}) refute author.deleted refute author.deleted_at - assert {:ok, deleted} = Author.delete(author.id) - assert deleted.deleted - assert deleted.deleted_at - assert [deleted] == Author.all() + assert :ok = Author.delete(author.id) + assert [] = Author.all() end + test "get an author with bad id" do + assert {:error, "Invalid :binary_id with value \"1\""} = Author.delete("1") + end + + test "delete an author with bad id" do + assert {:error, "Invalid :binary_id with value \"1\""} = Author.delete("1") + end end From 5f053ae5799f781e7f8922083c3111461ae805bd Mon Sep 17 00:00:00 2001 From: Johnny Winn Date: Sun, 22 Oct 2017 18:36:42 -0600 Subject: [PATCH 3/4] Add GTAGS files to .gitignore --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index 12179ea..0941a92 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,7 @@ erl_crash.dump # Also ignore archive artifacts (built via "mix archive.build"). *.ez + +GPATH +GRTAGS +GTAGS \ No newline at end of file From 36b19228e4dc0a519bf063c205347803c2cf8732 Mon Sep 17 00:00:00 2001 From: Johnny Winn Date: Sun, 22 Oct 2017 22:10:41 -0600 Subject: [PATCH 4/4] Dashboard returns JSON-API response --- apps/api/lib/api_web/views/dashboard_view.ex | 3 ++ apps/core/lib/core/data/dashboard.ex | 2 +- apps/core/lib/core/schemas/dashboard.ex | 6 +-- apps/core/test/core/data/dashboard_test.exs | 56 ++++++++++++++++++++ 4 files changed, 63 insertions(+), 4 deletions(-) create mode 100644 apps/core/test/core/data/dashboard_test.exs diff --git a/apps/api/lib/api_web/views/dashboard_view.ex b/apps/api/lib/api_web/views/dashboard_view.ex index 6765015..4c40214 100644 --- a/apps/api/lib/api_web/views/dashboard_view.ex +++ b/apps/api/lib/api_web/views/dashboard_view.ex @@ -13,4 +13,7 @@ defmodule ApiWeb.Api.V1.DashboardView do serializer: ApiWeb.Api.V1.AuthorView, include: true + has_many :widgets, + serializer: ApiWeb.Api.V1.WidgetView, + include: true end diff --git a/apps/core/lib/core/data/dashboard.ex b/apps/core/lib/core/data/dashboard.ex index b674cc2..5180ca3 100644 --- a/apps/core/lib/core/data/dashboard.ex +++ b/apps/core/lib/core/data/dashboard.ex @@ -1,5 +1,5 @@ defmodule Core.Data.Dashboard do use Core.Data.Query, schema: Core.Schemas.Dashboard, - preloads: [:author] + preloads: [:author, [widgets: [:author, :data_sources]]] end diff --git a/apps/core/lib/core/schemas/dashboard.ex b/apps/core/lib/core/schemas/dashboard.ex index 3dbd37e..0d79d37 100644 --- a/apps/core/lib/core/schemas/dashboard.ex +++ b/apps/core/lib/core/schemas/dashboard.ex @@ -30,7 +30,7 @@ defmodule Core.Schemas.Dashboard do belongs_to :author, Author, type: Ecto.UUID has_many :dashboard_widgets, DashboardWidget, on_delete: :delete_all, on_replace: :delete - has_many :widgets, through: [:dashboard_widgets, :widgets] + has_many :widgets, through: [:dashboard_widgets, :widget] timestamps type: :utc_datetime end @@ -40,13 +40,13 @@ defmodule Core.Schemas.Dashboard do def changeset(model, params \\ %{}) - def changeset(model, %{"dashboard_widgets" => widgets} = params) when is_list(widgets) do + def changeset(model, %{"dashboard_widgets" => dashboard_widgets} = params) when is_list(dashboard_widgets) do model |> cast(params, @required_fields ++ @optional_fields) |> validate_required(@required_fields) |> foreign_key_constraint(:author_id) |> unique_constraint(:dashboard, name: @unique_index, message: @unique_error) - |> cast_assoc(:dashboard_widgets, params["dashboard_widgets"]) + |> cast_assoc(:dashboard_widgets, dashboard_widgets) end def changeset(model, params) do diff --git a/apps/core/test/core/data/dashboard_test.exs b/apps/core/test/core/data/dashboard_test.exs new file mode 100644 index 0000000..d8d7383 --- /dev/null +++ b/apps/core/test/core/data/dashboard_test.exs @@ -0,0 +1,56 @@ +defmodule Core.Data.DashboardTest do + use Core.DataCase + + @valid_attrs %{"name" => "Dashboard 1", "slug" => "dashboard-1"} + + import Core.DataHelper, only: [generate_author: 0, generate_widget: 1] + + setup do + author = generate_author() + widget = generate_widget(author) + {:ok, [author: author, widget: widget]} + end + + alias Core.Data.Dashboard + + test "get an dashboard by id", %{author: author, widget: widget} do + attrs = Map.merge(@valid_attrs, %{"author_id" => author.id, "dashboard_widgets" => [%{"widget_id" => widget.id}]} ) + assert {:ok, dashboard} = Dashboard.upsert(attrs) + assert {:ok, dashboard} == Dashboard.get(dashboard.id) + end + + test "create a new dashboard with valid params", %{author: author, widget: widget} do + assert [] = Dashboard.all() + attrs = Map.merge(@valid_attrs, %{"author_id" => author.id, "dashboard_widgets" => [%{"widget_id" => widget.id}]} ) + assert {:ok, dashboard} = Dashboard.upsert(attrs) + assert dashboard.widgets + assert [dashboard] == Dashboard.all() + end + + test "edit an existing dashboard with valid params", %{author: author, widget: widget} do + assert [] = Dashboard.all() + attrs = Map.merge(@valid_attrs, %{"author_id" => author.id}) + assert {:ok, dashboard} = Dashboard.upsert(attrs) + refute dashboard.image_src + assert {:ok, updated} = Dashboard.upsert(%{"id" => dashboard.id, "description" => "Updated description"}) + assert updated.id == dashboard.id + assert updated.description == "Updated description" + assert [updated] == Dashboard.all() + end + + test "delete an existing dashboard" do + {:ok, dashboard} = Dashboard.upsert(%{"username" => "bob-dobbs"}) + refute dashboard.deleted + refute dashboard.deleted_at + assert :ok = Dashboard.delete(dashboard.id) + assert [] = Dashboard.all() + end + + test "get an dashboard with bad id" do + assert {:error, "Invalid :binary_id with value \"1\""} = Dashboard.delete("1") + end + + test "delete an dashboard with bad id" do + assert {:error, "Invalid :binary_id with value \"1\""} = Dashboard.delete("1") + end +end