diff --git a/lib/ash/plug_helpers.ex b/lib/ash/plug_helpers.ex new file mode 100644 index 00000000..750d54ee --- /dev/null +++ b/lib/ash/plug_helpers.ex @@ -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 diff --git a/mix.exs b/mix.exs index 9a71edc5..ea52e7bb 100644 --- a/mix.exs +++ b/mix.exs @@ -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 diff --git a/mix.lock b/mix.lock index c1ec4988..fae4bfb1 100644 --- a/mix.lock +++ b/mix.lock @@ -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"}, diff --git a/test/plug_helpers.ex b/test/plug_helpers.ex new file mode 100644 index 00000000..0df864fd --- /dev/null +++ b/test/plug_helpers.ex @@ -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