Skip to content
This repository has been archived by the owner on Jun 30, 2021. It is now read-only.

Commit

Permalink
Introduce Overlay to improve preloading/filtering (#486)
Browse files Browse the repository at this point in the history
* Add preload fields in serialisers

* Add Overlay and Orchestrator

* Set up AccountOverlay and MembershipOverlay

* Create all overlays, handle filters and implement TransactionOverlay

* Prevent warnings from breaking in test ENV

* Overlays for AuthToken and API Key

* Implement ExchangePairOverlay

* Refactor CategoryController with Overlay

* Add filters to CategoryOverlay

* Remove preloading from ExchangePairSerializer

* Refactor KeyController with KeyOverlay

* Refactor MembershipController to work with MembershipOverlay

* Add preloading to KeyOverlay

* Refactor MintController with MintOverlay

* Refactor Consumption controllers with ConsumptionOverlay

* Add filter fields to consumption overlay

* Use overlay for transaction request / consumptions

* Update TransactionController-s with overlay

* Refactor AuthController with Overlay

* Use Overlay for Wallets/Users

* Add module docs

* Mix Credo issues

* Refactor TokenController with overlay

* Remove unneeded aliases

* Fix format

* Add preloading for exchange pair in transaction overlay

* Mix format 1.6.5

* Remove Embedder

* Update doc for Orchestrator

* Add missing Behaviour to overlay

* Remove Embedder tests
  • Loading branch information
Thibault authored Oct 9, 2018
1 parent 4bccae1 commit 801d30c
Show file tree
Hide file tree
Showing 84 changed files with 1,548 additions and 903 deletions.
1 change: 1 addition & 0 deletions .credo.exs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
#
## Readability Checks
#
{Credo.Check.Readability.AliasOrder, false},
{Credo.Check.Readability.FunctionNames},
{Credo.Check.Readability.LargeNumbers},
{Credo.Check.Readability.MaxLineLength, priority: :low, max_length: 100},
Expand Down
43 changes: 8 additions & 35 deletions apps/admin_api/lib/admin_api/v1/controllers/account_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,9 @@ defmodule AdminAPI.V1.AccountController do
import AdminAPI.V1.ErrorHandler
alias AdminAPI.V1.AccountHelper
alias EWallet.AccountPolicy
alias EWallet.Web.{Paginator, Preloader, SearchParser, SortParser}
alias EWallet.Web.{Orchestrator, Paginator, V1.AccountOverlay}
alias EWalletDB.Account

# The field names to be mapped into DB column names.
# The keys and values must be strings as this is mapped early before
# any operations are done on the field names. For example:
# `"request_field_name" => "db_column_name"`
@mapped_fields %{
"created_at" => "inserted_at"
}

# The fields that should be preloaded.
# Note that these values *must be in the schema associations*.
@preload_fields [:categories]

# The fields that are allowed to be searched.
# Note that these values here *must be the DB column names*
# If the request provides different names, map it via `@mapped_fields` first.
@search_fields [:id, :name, :description]

# The fields that are allowed to be sorted.
# Note that the values here *must be the DB column names*.
# If the request provides different names, map it via `@mapped_fields` first.
@sort_fields [:id, :name, :description, :inserted_at, :updated_at]

@doc """
Retrieves a list of accounts based on current account for users.
"""
Expand All @@ -38,10 +16,7 @@ defmodule AdminAPI.V1.AccountController do
# Get all the accounts the current accessor has access to
Account
|> Account.where_in(account_uuids)
|> Preloader.to_query(@preload_fields)
|> SearchParser.to_query(attrs, @search_fields, @mapped_fields)
|> SortParser.to_query(attrs, @sort_fields, @mapped_fields)
|> Paginator.paginate_attrs(attrs)
|> Orchestrator.query(AccountOverlay, attrs)
|> respond(conn)
else
error -> respond(error, conn)
Expand All @@ -56,9 +31,7 @@ defmodule AdminAPI.V1.AccountController do
# Get all users since everyone can access them
Account
|> Account.where_in(descendant_uuids)
|> SearchParser.to_query(attrs, @search_fields)
|> SortParser.to_query(attrs, @sort_fields, @mapped_fields)
|> Paginator.paginate_attrs(attrs)
|> Orchestrator.query(AccountOverlay, attrs)
|> respond(conn)
else
error -> respond(error, conn)
Expand All @@ -71,10 +44,10 @@ defmodule AdminAPI.V1.AccountController do
Retrieves a specific account by its id.
"""
@spec get(Plug.Conn.t(), map()) :: Plug.Conn.t()
def get(conn, %{"id" => id}) do
def get(conn, %{"id" => id} = attrs) do
with %Account{} = account <- Account.get_by(id: id) || {:error, :unauthorized},
:ok <- permit(:get, conn.assigns, account.id),
{:ok, account} <- Preloader.preload_one(account, @preload_fields) do
{:ok, account} <- Orchestrator.one(account, AccountOverlay, attrs) do
render(conn, :account, %{account: account})
else
{:error, code} ->
Expand Down Expand Up @@ -102,7 +75,7 @@ defmodule AdminAPI.V1.AccountController do
with :ok <- permit(:create, conn.assigns, parent.id),
attrs <- Map.put(attrs, "parent_uuid", parent.uuid),
{:ok, account} <- Account.insert(attrs),
{:ok, account} <- Preloader.preload_one(account, @preload_fields) do
{:ok, account} <- Orchestrator.one(account, AccountOverlay, attrs) do
render(conn, :account, %{account: account})
else
{:error, %{} = changeset} ->
Expand All @@ -123,7 +96,7 @@ defmodule AdminAPI.V1.AccountController do
with %Account{} = original <- Account.get(account_id) || {:error, :unauthorized},
:ok <- permit(:update, conn.assigns, original.id),
{:ok, updated} <- Account.update(original, attrs),
{:ok, updated} <- Preloader.preload_one(updated, @preload_fields) do
{:ok, updated} <- Orchestrator.one(updated, AccountOverlay, attrs) do
render(conn, :account, %{account: updated})
else
{:error, %{} = changeset} ->
Expand All @@ -144,7 +117,7 @@ defmodule AdminAPI.V1.AccountController do
with %Account{} = account <- Account.get(id) || {:error, :unauthorized},
:ok <- permit(:update, conn.assigns, account.id),
%{} = saved <- Account.store_avatar(account, attrs),
{:ok, saved} <- Preloader.preload_one(saved, @preload_fields) do
{:ok, saved} <- Orchestrator.one(saved, AccountOverlay, attrs) do
render(conn, :account, %{account: saved})
else
nil ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,21 @@ defmodule AdminAPI.V1.AccountMembershipController do
import AdminAPI.V1.ErrorHandler
alias AdminAPI.InviteEmail
alias EWallet.{AccountMembershipPolicy, EmailValidator}
alias EWallet.Web.{Inviter, Originator, UrlValidator}
alias EWalletDB.{Account, Membership, Role, User}
alias EWallet.Web.{Inviter, Orchestrator, Originator, UrlValidator, V1.MembershipOverlay}
alias EWalletDB.{Account, Membership, Repo, Role, User}

@doc """
Lists the users that are assigned to the given account.
"""
def all_for_account(conn, %{"id" => account_id}) do
def all_for_account(conn, %{"id" => account_id} = attrs) do
with %Account{} = account <-
Account.get(account_id, preload: [memberships: [:user, :role]]) ||
{:error, :unauthorized},
:ok <- permit(:get, conn.assigns, account.id),
ancestor_uuids <- Account.get_all_ancestors_uuids(account),
memberships <- Membership.all_by_account_uuids(ancestor_uuids, [:role, :account, :user]),
query <- Membership.all_by_account_uuids(ancestor_uuids),
query <- Orchestrator.build_query(query, MembershipOverlay, attrs),
memberships <- Repo.all(query),
memberships <- Membership.distinct_by_role(memberships) do
render(conn, :memberships, %{memberships: memberships})
else
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ defmodule AdminAPI.V1.AdminAuthController do
import AdminAPI.V1.ErrorHandler
alias AdminAPI.V1.AdminUserAuthenticator
alias EWallet.AccountPolicy
alias EWallet.Web.{Orchestrator, V1.AuthTokenOverlay}
alias EWalletDB.{Account, AuthToken, User}

@doc """
Expand All @@ -15,22 +16,24 @@ defmodule AdminAPI.V1.AdminAuthController do
conn <- AdminUserAuthenticator.authenticate(conn, attrs["email"], attrs["password"]),
true <- conn.assigns.authenticated || {:error, :invalid_login_credentials},
true <- User.get_status(conn.assigns.admin_user) == :active || {:error, :invite_pending},
{:ok, auth_token} = AuthToken.generate(conn.assigns.admin_user, :admin_api) do
{:ok, auth_token} = AuthToken.generate(conn.assigns.admin_user, :admin_api),
{:ok, auth_token} <- Orchestrator.one(auth_token, AuthTokenOverlay, attrs) do
render_token(conn, auth_token)
else
{:error, code} when is_atom(code) ->
handle_error(conn, code)
end
end

def switch_account(conn, %{"account_id" => account_id}) do
def switch_account(conn, %{"account_id" => account_id} = attrs) do
with {:ok, _current_user} <- permit(:get, conn.assigns),
%Account{} = account <- Account.get(account_id) || {:error, :unauthorized},
:ok <- permit_account(:get, conn.assigns, account.id),
token <- conn.private.auth_auth_token,
%AuthToken{} = token <-
AuthToken.get_by_token(token, :admin_api) || {:error, :auth_token_not_found},
{:ok, token} <- AuthToken.switch_account(token, account) do
{:ok, token} <- AuthToken.switch_account(token, account),
{:ok, token} <- Orchestrator.one(token, AuthTokenOverlay, attrs) do
render_token(conn, token)
else
{:error, code} when is_atom(code) ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,9 @@ defmodule AdminAPI.V1.AdminUserController do
alias AdminAPI.V1.AccountHelper
alias AdminAPI.V1.UserView
alias EWallet.AdminUserPolicy
alias EWallet.Web.{Paginator, SearchParser, SortParser}
alias EWallet.Web.{Orchestrator, Paginator, V1.UserOverlay}
alias EWalletDB.{User, UserQuery}

# The field names to be mapped into DB column names.
# The keys and values must be strings as this is mapped early before
# any operations are done on the field names. For example:
# `"request_field_name" => "db_column_name"`
@mapped_fields %{
"created_at" => "inserted_at"
}

# The fields that are allowed to be searched.
# Note that these values here *must be the DB column names*
# Because requests cannot customize which fields to search (yet!),
# `@mapped_fields` don't affect them.
@search_fields [:id, :email]

# The fields that are allowed to be sorted.
# Note that the values here *must be the DB column names*.
# If the request provides different names, map it via `@mapped_fields` first.
@sort_fields [:id, :email, :inserted_at, :updated_at]

@doc """
Retrieves a list of admins that the current user/key has access to.
"""
Expand All @@ -35,9 +16,7 @@ defmodule AdminAPI.V1.AdminUserController do
account_uuids <- AccountHelper.get_accessible_account_uuids(conn.assigns) do
account_uuids
|> UserQuery.where_has_membership_in_accounts(User)
|> SearchParser.to_query(attrs, @search_fields)
|> SortParser.to_query(attrs, @sort_fields, @mapped_fields)
|> Paginator.paginate_attrs(attrs)
|> Orchestrator.query(UserOverlay, attrs)
|> respond_multiple(conn)
else
{:error, error} -> handle_error(conn, error)
Expand Down
50 changes: 10 additions & 40 deletions apps/admin_api/lib/admin_api/v1/controllers/api_key_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,38 +3,17 @@ defmodule AdminAPI.V1.APIKeyController do
import AdminAPI.V1.ErrorHandler
alias Ecto.Changeset
alias EWallet.APIKeyPolicy
alias EWallet.Web.{Paginator, SearchParser, SortParser}
alias EWallet.Web.{Orchestrator, Paginator, V1.APIKeyOverlay}
alias EWalletDB.APIKey

# The field names to be mapped into DB column names.
# The keys and values must be strings as this is mapped early before
# any operations are done on the field names. For example:
# `"request_field_name" => "db_column_name"`
@mapped_fields %{
"created_at" => "inserted_at"
}

# The fields that are allowed to be searched.
# Note that these values here *must be the DB column names*
# Because requests cannot customize which fields to search (yet!),
# `@mapped_fields` don't affect them.
@search_fields [:id, :key]

# The fields that are allowed to be sorted.
# Note that the values here *must be the DB column names*.
# If the request provides different names, map it via `@mapped_fields` first.
@sort_fields [:id, :key, :owner_app, :inserted_at, :updated_at]

@doc """
Retrieves a list of API keys including soft-deleted.
"""
@spec all(Plug.Conn.t(), map()) :: Plug.Conn.t()
def all(conn, attrs) do
with :ok <- permit(:all, conn.assigns, nil) do
APIKey
|> SearchParser.to_query(attrs, @search_fields)
|> SortParser.to_query(attrs, @sort_fields, @mapped_fields)
|> Paginator.paginate_attrs(attrs)
|> Orchestrator.query(APIKeyOverlay, attrs)
|> respond_multiple(conn)
else
{:error, code} -> handle_error(conn, code)
Expand All @@ -54,12 +33,12 @@ defmodule AdminAPI.V1.APIKeyController do
Creates a new API key. Currently API keys are assigned to the master account only.
"""
@spec create(Plug.Conn.t(), map()) :: Plug.Conn.t()
def create(conn, _attrs) do
with :ok <- permit(:create, conn.assigns, nil) do
# Admin API doesn't use API Keys anymore. Defaulting to "ewallet_api".
%{owner_app: "ewallet_api"}
|> APIKey.insert()
|> respond_single(conn)
def create(conn, attrs) do
with :ok <- permit(:create, conn.assigns, nil),
# Admin API doesn't use API Keys anymore. Defaulting to "ewallet_api".
{:ok, api_key} <- APIKey.insert(%{owner_app: "ewallet_api"}),
{:ok, api_key} <- Orchestrator.one(api_key, APIKeyOverlay, attrs) do
render(conn, :api_key, %{api_key: api_key})
else
{:error, code} ->
handle_error(conn, code)
Expand All @@ -73,7 +52,8 @@ defmodule AdminAPI.V1.APIKeyController do
def update(conn, %{"id" => id} = attrs) do
with :ok <- permit(:update, conn.assigns, id),
%APIKey{} = api_key <- APIKey.get(id) || :api_key_not_found,
{:ok, api_key} <- APIKey.update(api_key, attrs) do
{:ok, api_key} <- APIKey.update(api_key, attrs),
{:ok, api_key} <- Orchestrator.one(api_key, APIKeyOverlay, attrs) do
render(conn, :api_key, %{api_key: api_key})
else
error when is_atom(error) ->
Expand All @@ -91,16 +71,6 @@ defmodule AdminAPI.V1.APIKeyController do
handle_error(conn, :invalid_parameter)
end

# Respond when the API key is saved successfully
defp respond_single({:ok, api_key}, conn) do
render(conn, :api_key, %{api_key: api_key})
end

# Responds when the API key is saved unsucessfully
defp respond_single({:error, changeset}, conn) do
handle_error(conn, :invalid_parameter, changeset)
end

@doc """
Soft-deletes an existing API key by its id.
"""
Expand Down
55 changes: 13 additions & 42 deletions apps/admin_api/lib/admin_api/v1/controllers/category_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,50 +2,21 @@ defmodule AdminAPI.V1.CategoryController do
use AdminAPI, :controller
import AdminAPI.V1.ErrorHandler
alias EWallet.CategoryPolicy
alias EWallet.Web.{Paginator, Preloader, SearchParser, SortParser}
alias EWallet.Web.{Orchestrator, Paginator, V1.CategoryOverlay}
alias EWalletDB.Category

# The field names to be mapped into DB column names.
# The keys and values must be strings as this is mapped early before
# any operations are done on the field names. For example:
# `"request_field_name" => "db_column_name"`
@mapped_fields %{
"created_at" => "inserted_at"
}

# The fields that should be preloaded.
# Note that these values *must be in the schema associations*.
@preload_fields [:accounts]

# The fields that are allowed to be searched.
# Note that these values here *must be the DB column names*
# If the request provides different names, map it via `@mapped_fields` first.
@search_fields [:id, :name, :description]

# The fields that are allowed to be sorted.
# Note that the values here *must be the DB column names*.
# If the request provides different names, map it via `@mapped_fields` first.
@sort_fields [:id, :name, :description, :inserted_at, :updated_at]

@doc """
Retrieves a list of categories.
"""
@spec all(Plug.Conn.t(), map()) :: Plug.Conn.t()
def all(conn, attrs) do
with :ok <- permit(:all, conn.assigns, nil) do
Category
|> Preloader.to_query(@preload_fields)
|> SearchParser.to_query(attrs, @search_fields, @mapped_fields)
|> SortParser.to_query(attrs, @sort_fields, @mapped_fields)
|> Paginator.paginate_attrs(attrs)
|> case do
%Paginator{} = paginator ->
render(conn, :categories, %{categories: paginator})

{:error, code, description} ->
handle_error(conn, code, description)
end
with :ok <- permit(:all, conn.assigns, nil),
%Paginator{} = paginator <- Orchestrator.query(Category, CategoryOverlay, attrs) do
render(conn, :categories, %{categories: paginator})
else
{:error, code, description} ->
handle_error(conn, code, description)

{:error, code} ->
handle_error(conn, code)
end
Expand All @@ -55,10 +26,10 @@ defmodule AdminAPI.V1.CategoryController do
Retrieves a specific category by its id.
"""
@spec get(Plug.Conn.t(), map()) :: Plug.Conn.t()
def get(conn, %{"id" => id}) do
def get(conn, %{"id" => id} = attrs) do
with :ok <- permit(:get, conn.assigns, id),
%Category{} = category <- Category.get_by(id: id),
{:ok, category} <- Preloader.preload_one(category, @preload_fields) do
{:ok, category} <- Orchestrator.one(category, CategoryOverlay, attrs) do
render(conn, :category, %{category: category})
else
{:error, code} ->
Expand All @@ -76,7 +47,7 @@ defmodule AdminAPI.V1.CategoryController do
def create(conn, attrs) do
with :ok <- permit(:create, conn.assigns, nil),
{:ok, category} <- Category.insert(attrs),
{:ok, category} <- Preloader.preload_one(category, @preload_fields) do
{:ok, category} <- Orchestrator.one(category, CategoryOverlay, attrs) do
render(conn, :category, %{category: category})
else
{:error, %{} = changeset} ->
Expand All @@ -95,7 +66,7 @@ defmodule AdminAPI.V1.CategoryController do
with :ok <- permit(:update, conn.assigns, id),
%Category{} = original <- Category.get(id) || {:error, :category_id_not_found},
{:ok, updated} <- Category.update(original, attrs),
{:ok, updated} <- Preloader.preload_one(updated, @preload_fields) do
{:ok, updated} <- Orchestrator.one(updated, CategoryOverlay, attrs) do
render(conn, :category, %{category: updated})
else
{:error, %{} = changeset} ->
Expand All @@ -112,10 +83,10 @@ defmodule AdminAPI.V1.CategoryController do
Soft-deletes an existing category by its id.
"""
@spec delete(Plug.Conn.t(), map()) :: Plug.Conn.t()
def delete(conn, %{"id" => id}) do
def delete(conn, %{"id" => id} = attrs) do
with %Category{} = category <- Category.get(id) || {:error, :category_id_not_found},
{:ok, deleted} <- Category.delete(category),
{:ok, deleted} <- Preloader.preload_one(deleted, @preload_fields) do
{:ok, deleted} <- Orchestrator.one(deleted, CategoryOverlay, attrs) do
render(conn, :category, %{category: deleted})
else
{:error, %{} = changeset} ->
Expand Down
Loading

0 comments on commit 801d30c

Please sign in to comment.