feat(Ash.PlugHelpers): standardise conn interface for actors/tenants. (#432)

This commit is contained in:
James Harton 2022-10-28 11:52:42 +13:00 committed by GitHub
parent 3c6758fa59
commit 2d6762ae61
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 251 additions and 2 deletions

148
lib/ash/plug_helpers.ex Normal file
View file

@ -0,0 +1,148 @@
if Code.ensure_loaded?(Plug.Conn) do
defmodule Ash.PlugHelpers do
@moduledoc """
Helpers for working with the Plug connection.
"""
alias Plug.Conn
require Logger
defmacrop emit_assign_warning(type) do
quote do
Logger.warning(fn ->
{fun, arity} = __ENV__.function
file = Path.relative_to_cwd(__ENV__.file)
"""
Storing the #{unquote(type)} in conn assigns is deprecated.
#{file}:#{__ENV__.line}: #{inspect(__ENV__.module)}.#{fun}/#{arity}
"""
end)
end
end
@doc """
Sets the actor inside the Plug connection.
The actor is stored inside the [connection's private
fields](https://hexdocs.pm/plug/Plug.Conn.html#module-private-fields).
## Example
iex> actor = build_actor(%{email: "marty@1985.retro"})
...> conn = build_conn() |> set_actor(actor)
%Plug.Conn{private: %{ash: %{actor: %{email: "marty@1985.retro"}}}} = conn
"""
@spec set_actor(Conn.t(), Ash.Resource.record()) :: Conn.t()
def set_actor(conn, actor) do
ash_private =
conn.private
|> Map.get(:ash, %{})
|> Map.put(:actor, actor)
conn
|> Conn.put_private(:ash, ash_private)
end
@doc """
Retrieves the actor from the Plug connection.
The actor is stored inside the [connection's private
fields](https://hexdocs.pm/plug/Plug.Conn.html#module-private-fields).
## Deprecation warning
This function checks to see if the actor is already set in the `@actor`
assign, and if so will emit a deprecation warning.
This is to allow apps using the previous method a chance to update.
Rather than setting the actor in the assigns, please use the `set_actor/2`
method.
## Example
iex> actor = build_actor(%{email: "marty@1985.retro"})
...> conn = build_conn() |> put_private(:ash, %{actor: actor})
...> actor = get_actor(conn)
%{email: "marty@1985.retro"} = actor
iex> actor = build_actor(%{email: "marty@1985.retro"})
...> conn = build_conn() |> assign(:actor, actor)
...> actor = get_actor(conn)
%{email: "marty@1985.retro"} = actor
"""
@spec get_actor(Conn.t()) :: nil | Ash.Resource.record()
def get_actor(%{assigns: %{actor: actor}}) when not is_nil(actor) do
emit_assign_warning(:actor)
actor
end
def get_actor(%{private: %{ash: %{actor: actor}}}), do: actor
def get_actor(_), do: nil
@doc """
Sets the tenant inside the Plug connection.
The tenant is stored inside the [connection's private
fields](https://hexdocs.pm/plug/Plug.Conn.html#module-private-fields).
## Example
iex> tenant = build_tenant(%{name: "Deliver-yesterday"})
...> conn = build_conn() |> set_tenant(tenant)
%Plug.Conn{private: %{ash: %{tenant: %{name: "Deliver-yesterday"}}}} = conn
"""
@spec set_tenant(Conn.t(), Ash.Resource.record()) :: Conn.t()
def set_tenant(conn, tenant) do
ash_private =
conn.private
|> Map.get(:ash, %{})
|> Map.put(:tenant, tenant)
conn
|> Conn.put_private(:ash, ash_private)
end
@doc """
Retrieves the tenant from the Plug connection.
The tenant is stored inside the [connection's private
fields](https://hexdocs.pm/plug/Plug.Conn.html#module-private-fields).
## Deprecation warning
This function checks to see if the tenant is already set in the `@tenant`
assign, and if so will emit a deprecation warning.
This is to allow apps using the previous method a chance to update.
Rather than setting the tenant in the assigns, please use the `set_tenant/2`
method.
## Example
iex> tenant = build_tenant(%{name: "Deliver-yesterday"})
...> conn = build_conn() |> put_private(:ash, %{tenant: tenant})
...> tenant = get_tenant(conn)
%{name: "Deliver-yesterday"} = tenant
iex> tenant = build_tenant(%{name: "Deliver-yesterday"})
...> conn = build_conn() |> assign(:tenant, tenant)
...> tenant = get_tenant(conn)
%{name: "Deliver-yesterday"} = tenant
"""
@spec get_tenant(Conn.t()) :: nil | Ash.Resource.record()
def get_tenant(%{assigns: %{tenant: tenant}}) when not is_nil(tenant) do
emit_assign_warning(:tenant)
tenant
end
def get_tenant(%{private: %{ash: %{tenant: tenant}}}), do: tenant
def get_tenant(_), do: nil
end
end

View file

@ -19,7 +19,7 @@ defmodule Ash.MixProject do
elixirc_paths: elixirc_paths(Mix.env()),
package: package(),
deps: deps(),
dialyzer: [plt_add_apps: [:mix, :mnesia, :earmark]],
dialyzer: [plt_add_apps: [:mix, :mnesia, :earmark, :plug]],
xref: [exclude: [:mnesia]],
docs: docs(),
aliases: aliases(),
@ -238,7 +238,8 @@ defmodule Ash.MixProject do
{:git_ops, "~> 2.5", only: :dev},
{:mix_test_watch, "~> 1.0", only: :dev, runtime: false},
{:parse_trans, "3.3.0", only: [:dev, :test], override: true},
{:elixir_sense, github: "elixir-lsp/elixir_sense", only: [:dev, :test, :docs]}
{:elixir_sense, github: "elixir-lsp/elixir_sense", only: [:dev, :test, :docs]},
{:plug, ">= 0.0.0", only: [:dev, :test], runtime: false}
]
end

View file

@ -27,12 +27,15 @@
"makeup_elixir": {:hex, :makeup_elixir, "0.16.0", "f8c570a0d33f8039513fbccaf7108c5d750f47d8defd44088371191b76492b0b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "28b2cbdc13960a46ae9a8858c4bebdec3c9a6d7b4b9e7f4ed1502f8159f338e7"},
"makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
"mime": {:hex, :mime, "2.0.3", "3676436d3d1f7b81b5a2d2bd8405f412c677558c81b1c92be58c00562bb59095", [:mix], [], "hexpm", "27a30bf0db44d25eecba73755acf4068cbfe26a4372f9eb3e4ea3a45956bff6b"},
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
"mix_test_watch": {:hex, :mix_test_watch, "1.1.0", "330bb91c8ed271fe408c42d07e0773340a7938d8a0d281d57a14243eae9dc8c3", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm", "52b6b1c476cbb70fd899ca5394506482f12e5f6b0d6acff9df95c7f1e0812ec3"},
"nimble_options": {:hex, :nimble_options, "0.4.0", "c89babbab52221a24b8d1ff9e7d838be70f0d871be823165c94dd3418eea728f", [:mix], [], "hexpm", "e6701c1af326a11eea9634a3b1c62b475339ace9456c1a23ec3bc9a847bca02d"},
"nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"},
"parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"},
"picosat_elixir": {:hex, :picosat_elixir, "0.2.0", "1c9d2d65b32039c9e3eb600bff903579e5916f559dbf0013b3c4ca617c93ac64", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "7900c6d58d65a9d8a2899f53019fb70e9b9678161bbb53f646e28e146aca138c"},
"plug": {:hex, :plug, "1.13.6", "187beb6b67c6cec50503e940f0434ea4692b19384d47e5fdfd701e93cadb4cc2", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "02b9c6b9955bce92c829f31d6284bf53c591ca63c4fb9ff81dfd0418667a34ff"},
"plug_crypto": {:hex, :plug_crypto, "1.2.3", "8f77d13aeb32bfd9e654cb68f0af517b371fb34c56c9f2b58fe3df1235c1251a", [:mix], [], "hexpm", "b5672099c6ad5c202c45f5a403f21a3411247f164e4a8fab056e5cd8a290f4a2"},
"providers": {:hex, :providers, "1.8.1", "70b4197869514344a8a60e2b2a4ef41ca03def43cfb1712ecf076a0f3c62f083", [:rebar3], [{:getopt, "1.0.1", [hex: :getopt, repo: "hexpm", optional: false]}], "hexpm", "e45745ade9c476a9a469ea0840e418ab19360dc44f01a233304e118a44486ba0"},
"sobelow": {:hex, :sobelow, "0.11.1", "23438964486f8112b41e743bbfd402da3e5b296fdc9eacab29914b79c48916dd", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "9897363a7eff96f4809304a90aad819e2ad5e5d24db547af502885146746a53c"},
"sourceror": {:hex, :sourceror, "0.11.2", "549ce48be666421ac60cfb7f59c8752e0d393baa0b14d06271d3f6a8c1b027ab", [:mix], [], "hexpm", "9ab659118896a36be6eec68ff7b0674cba372fc8e210b1e9dc8cf2b55bb70dfb"},

97
test/plug_helpers.ex Normal file
View file

@ -0,0 +1,97 @@
defmodule Ash.Test.PlugHelpersTest do
@moduledoc false
use ExUnit.Case, async: true
alias Ash.Changeset
import Ash.PlugHelpers
import Plug.Conn
def build_conn, do: Plug.Test.conn(:get, "/")
defmodule User do
@moduledoc false
use Ash.Resource, data_layer: Ash.DataLayer.Ets
ets do
private? true
end
actions do
read :read
create :create
end
attributes do
uuid_primary_key :id
attribute :email, :string
end
multitenancy do
strategy :attribute
attribute :customer_id
end
relationships do
belongs_to :customer, Customer
end
end
defmodule Customer do
@moduledoc false
use Ash.Resource, data_layer: Ash.DataLayer.Ets
ets do
private? true
end
actions do
read :read
create :create
end
attributes do
uuid_primary_key :id
attribute :name, :string
end
relationships do
has_many :users, User
end
end
defmodule Registry do
@moduledoc false
use Ash.Registry
entries do
entry Customer
entry User
end
end
defmodule Api do
@moduledoc false
use Ash.Api
resources do
registry Registry
end
end
def build_actor(attrs) do
attrs =
attrs
|> Map.put_new_lazy(:customer_id, fn -> build_tenant(%{name: "Deliver-yesterday"}).id end)
User
|> Changeset.for_create(:create, attrs, tenant: attrs.customer_id)
|> Api.create!()
end
def build_tenant(attrs) do
Customer
|> Changeset.for_create(:create, attrs)
|> Api.create!()
end
doctest Ash.PlugHelpers
end