From f366c834aaba9dca97fb120f9b291b1e30ab0ab2 Mon Sep 17 00:00:00 2001 From: APB9785 <74077809+APB9785@users.noreply.github.com> Date: Mon, 11 Dec 2023 10:52:25 -0600 Subject: [PATCH] Save state --- lib/beacon/content.ex | 101 ++++++++++++------ .../{live_data_path.ex => live_data.ex} | 14 ++- lib/beacon/content/live_data_assign.ex | 18 ++-- lib/beacon/pub_sub.ex | 9 ++ .../20231006223956_create_live_data.exs | 3 - ...0231211162418_create_live_data_assigns.exs | 17 +++ test/beacon/content_test.exs | 88 +++++++++++++++ test/support/fixtures.ex | 19 +++- 8 files changed, 214 insertions(+), 55 deletions(-) rename lib/beacon/content/{live_data_path.ex => live_data.ex} (70%) create mode 100644 priv/repo/migrations/20231211162418_create_live_data_assigns.exs diff --git a/lib/beacon/content.ex b/lib/beacon/content.ex index 9d2eb10b..48c14fb2 100644 --- a/lib/beacon/content.ex +++ b/lib/beacon/content.ex @@ -31,6 +31,7 @@ defmodule Beacon.Content do alias Beacon.Content.LayoutEvent alias Beacon.Content.LayoutSnapshot alias Beacon.Content.LiveData + alias Beacon.Content.LiveDataAssign alias Beacon.Content.Page alias Beacon.Content.PageEvent alias Beacon.Content.PageEventHandler @@ -2039,29 +2040,29 @@ defmodule Beacon.Content do # LIVE DATA @doc """ - Returns a list of all existing LiveData formats. + Returns a list of all existing LiveDataAssign formats. """ @doc type: :live_data - @spec live_data_formats() :: [atom()] - def live_data_formats, do: LiveData.formats() + @spec live_data_assign_formats() :: [atom()] + def live_data_assign_formats, do: LiveDataAssign.formats() @doc """ - Returns an `%Ecto.Changeset{}` for tracking LiveData changes. + Returns an `%Ecto.Changeset{}` for tracking LiveData `:path` changes. ## Example - iex> change_live_data(live_data, %{code: "false"}) + iex> change_live_data(live_data, %{path: "/foo/:bar_id"}) %Ecto.Changeset{data: %LiveData{}} """ @doc type: :live_data - @spec change_live_data(LiveData.t(), map()) :: Changeset.t() - def change_live_data(%LiveData{} = live_data, attrs \\ %{}) do - LiveData.changeset(live_data, attrs) + @spec change_live_data_path(LiveData.t(), map()) :: Changeset.t() + def change_live_data_path(%LiveData{} = live_data, attrs \\ %{}) do + LiveData.path_changeset(live_data, attrs) end @doc """ - Creates new assigns for Page templates using user-inputted code. + Creates a new LiveData for scoping live data to pages. """ @doc type: :live_data @spec create_live_data(map()) :: {:ok, LiveData.t()} | {:error, Changeset.t()} @@ -2073,18 +2074,40 @@ defmodule Beacon.Content do end @doc """ - Gets a single LiveData entry by `id`. + Creates a new LiveDataAssign. + """ + @doc type: :live_data + @spec create_assign_for_live_data(LiveData.t(), map()) :: {:ok, LiveData.t()} | {:error, Changeset.t()} + def create_assign_for_live_data(live_data, attrs) do + changeset = + live_data + |> Ecto.build_assoc(:assigns) + |> LiveDataAssign.changeset(attrs) + + case Repo.insert(changeset) do + {:ok, %LiveDataAssign{}} -> + live_data = Repo.preload(live_data, :assigns, force: true) + maybe_reload_live_data({:ok, live_data}) + {:ok, live_data} + + {:error, changeset} -> + {:error, changeset} + end + end + + @doc """ + Gets a single `LiveData` entry by `:site` and `:path`. ## Example - iex> get_live_data("788b2161-b23a-48ed-abcd-8af788004bbb") + iex> get_live_data(:my_site, "/foo/bar/:baz") %LiveData{} """ @doc type: :live_data - @spec get_live_data(Ecto.UUID.t()) :: LiveData.t() | nil - def get_live_data(id) when is_binary(id) do - Repo.get(LiveData, id) + @spec get_live_data(Site.t(), String.t()) :: LiveData.t() | nil + def get_live_data(site, path) do + Repo.get_by(LiveData, site: site, path: path) end @doc """ @@ -2093,11 +2116,11 @@ defmodule Beacon.Content do @doc type: :live_data @spec live_data_for_site(Site.t()) :: [LiveData.t()] def live_data_for_site(site) do - Repo.all(from ld in LiveData, where: ld.site == ^site) + Repo.all(from ld in LiveData, where: ld.site == ^site, preload: :assigns) end @doc """ - Query paths with LiveData for a given site. + Query LiveData paths for a given site. ## Options @@ -2108,7 +2131,7 @@ defmodule Beacon.Content do @doc type: :live_data @spec live_data_paths_for_site(Site.t(), Keyword.t()) :: [String.t()] def live_data_paths_for_site(site, opts \\ []) do - per_page = Keyword.get(opts, :per_page, 20) + per_page = Keyword.get(opts, :per_page, :infinity) search = Keyword.get(opts, :query) site @@ -2122,7 +2145,6 @@ defmodule Beacon.Content do from ld in LiveData, where: ld.site == ^site, select: ld.path, - distinct: ld.path, order_by: [asc: ld.path] end @@ -2133,26 +2155,34 @@ defmodule Beacon.Content do defp query_live_data_paths_for_site_search(query, _search), do: query @doc """ - Gets all LiveData for a single path of a given site. + Updates LiveDataPath. + + iex> update_live_data_path(live_data, "/foo/bar/:baz_id") + {:ok, %LiveData{}} + """ @doc type: :live_data - @spec live_data_for_path(Site.t(), String.t()) :: [LiveData.t()] - def live_data_for_path(site, path) do - Repo.all(from ld in LiveData, where: ld.path == ^path and ld.site == ^site) + @spec update_live_data_path(LiveData.t(), String.t()) :: {:ok, LiveData.t()} | {:error, Changeset.t()} + def update_live_data_path(%LiveData{} = live_data, path) do + live_data + |> LiveData.path_changeset(%{path: path}) + |> Repo.update() + |> tap(&maybe_reload_live_data/1) end @doc """ - Updates LiveData. + Updates LiveDataAssign. - iex> update_live_data(live_data, %{code: "true"}) - {:ok, %LiveData{}} + iex> update_live_data_assign(live_data_assign, %{code: "true"}) + {:ok, %LiveDataAssign{}} """ @doc type: :live_data - @spec update_live_data(LiveData.t(), map()) :: {:ok, LiveData.t()} | {:error, Changeset.t()} - def update_live_data(%LiveData{} = live_data, attrs) do - live_data - |> LiveData.changeset(attrs) + @spec update_live_data_assign(LiveDataAssign.t(), map()) :: {:ok, LiveDataAssign.t()} | {:error, Changeset.t()} + def update_live_data_assign(%LiveDataAssign{} = live_data_assign, attrs) do + live_data_assign + |> Repo.preload(:live_data) + |> LiveDataAssign.changeset(attrs) |> validate_live_data_code() |> Repo.update() |> tap(&maybe_reload_live_data/1) @@ -2160,9 +2190,9 @@ defmodule Beacon.Content do defp validate_live_data_code(changeset) do site = Changeset.get_field(changeset, :site) - code = Changeset.get_field(changeset, :code) + value = Changeset.get_field(changeset, :value) metadata = %Beacon.Template.LoadMetadata{site: site, path: "nopath"} - do_validate_template(changeset, :code, :heex, code, metadata) + do_validate_template(changeset, :value, :heex, value, metadata) end def maybe_reload_live_data({:ok, live_data}), do: PubSub.live_data_updated(live_data) @@ -2177,6 +2207,15 @@ defmodule Beacon.Content do Repo.delete(live_data) end + @doc """ + Deletes LiveDataAssign. + """ + @doc type: :live_data + @spec delete_live_data_assign(LiveDataAssign.t()) :: {:ok, LiveDataAssign.t()} | {:error, Changeset.t()} + def delete_live_data_assign(live_data_assign) do + Repo.delete(live_data_assign) + end + ## Utils defp do_validate_template(changeset, field, _format, nil = _template, _metadata) do diff --git a/lib/beacon/content/live_data_path.ex b/lib/beacon/content/live_data.ex similarity index 70% rename from lib/beacon/content/live_data_path.ex rename to lib/beacon/content/live_data.ex index 94f548fb..8030cf69 100644 --- a/lib/beacon/content/live_data_path.ex +++ b/lib/beacon/content/live_data.ex @@ -1,6 +1,6 @@ -defmodule Beacon.Content.LiveDataPath do +defmodule Beacon.Content.LiveData do @moduledoc """ - Dynamic assigns to be used by page templates and updated with page event handlers. + The LiveData schema scopes `LiveDataAssign`s to `Page`s via site and path. > #### Do not create or edit live data manually {: .warning} > @@ -20,21 +20,19 @@ defmodule Beacon.Content.LiveDataPath do id: Ecto.UUID.t(), site: Beacon.Types.Site.t(), path: String.t(), - live_data_assigns: [LiveDataAssign] + assigns: [LiveDataAssign] } - @formats [:text, :elixir] - schema "beacon_live_data" do field :site, Beacon.Types.Site field :path, :string - has_many :live_data_assigns, LiveDataAssign + has_many :assigns, LiveDataAssign timestamps() end - def changeset(%__MODULE__{} = live_data_path, attrs) do + def changeset(%__MODULE__{} = live_data, attrs) do fields = ~w(site path)a live_data @@ -42,7 +40,7 @@ defmodule Beacon.Content.LiveDataPath do |> validate_required(fields) end - def path_changeset(%__MODULE__{} = live_data_path, attrs) do + def path_changeset(%__MODULE__{} = live_data, attrs) do live_data |> cast(attrs, [:path]) |> validate_required([:path]) diff --git a/lib/beacon/content/live_data_assign.ex b/lib/beacon/content/live_data_assign.ex index c0a2728c..aa0dbe4d 100644 --- a/lib/beacon/content/live_data_assign.ex +++ b/lib/beacon/content/live_data_assign.ex @@ -14,31 +14,31 @@ defmodule Beacon.Content.LiveDataAssign do use Beacon.Schema - alias Beacon.Content.LiveDataPath + alias Beacon.Content.LiveData @type t :: %__MODULE__{ id: Ecto.UUID.t(), - live_data_path_id: Ecto.UUID.t(), - live_data_path: LiveDataPath.t(), - assign: String.t(), + key: String.t(), + value: String.t(), format: :text | :elixir, - code: String.t() + live_data_id: Ecto.UUID.t(), + live_data: LiveData.t() } @formats [:text, :elixir] schema "beacon_live_data_assigns" do - field :assign, :string + field :key, :string + field :value, :string field :format, Ecto.Enum, values: @formats - field :code, :string - belongs_to :live_data_path, LiveDataPath + belongs_to :live_data, LiveData timestamps() end def changeset(%__MODULE__{} = live_data_assign, attrs) do - fields = ~w(assign format code live_data_path_id)a + fields = ~w(key value format live_data_id)a live_data_assign |> cast(attrs, fields) diff --git a/lib/beacon/pub_sub.ex b/lib/beacon/pub_sub.ex index a201935f..172267f3 100644 --- a/lib/beacon/pub_sub.ex +++ b/lib/beacon/pub_sub.ex @@ -6,6 +6,7 @@ defmodule Beacon.PubSub do alias Beacon.Content.ErrorPage alias Beacon.Content.Layout alias Beacon.Content.LiveData + alias Beacon.Content.LiveDataAssign alias Beacon.Content.Page @pubsub __MODULE__ @@ -172,6 +173,14 @@ defmodule Beacon.PubSub do |> broadcast(:live_data_updated) end + def live_data_updated(%LiveDataAssign{} = live_data_assign) do + %{live_data: %{site: site}} = Beacon.Repo.preload(live_data_assign, :live_data) + + site + |> topic_live_data() + |> broadcast(:live_data_updated) + end + defp topic_live_data(site), do: "beacon:#{site}:live_data" # Utils diff --git a/priv/repo/migrations/20231006223956_create_live_data.exs b/priv/repo/migrations/20231006223956_create_live_data.exs index fdc701b7..577ee0f3 100644 --- a/priv/repo/migrations/20231006223956_create_live_data.exs +++ b/priv/repo/migrations/20231006223956_create_live_data.exs @@ -6,9 +6,6 @@ defmodule Beacon.Repo.Migrations.CreateLiveData do add :id, :binary_id, primary_key: true add :site, :text, null: false add :path, :text, null: false - add :assign, :text, null: false - add :code, :text, null: false - add :format, :string, null: false timestamps() end diff --git a/priv/repo/migrations/20231211162418_create_live_data_assigns.exs b/priv/repo/migrations/20231211162418_create_live_data_assigns.exs new file mode 100644 index 00000000..5b2d2783 --- /dev/null +++ b/priv/repo/migrations/20231211162418_create_live_data_assigns.exs @@ -0,0 +1,17 @@ +defmodule Beacon.Repo.Migrations.CreateLiveDataAssigns do + use Ecto.Migration + + def change do + create table(:beacon_live_data_assigns, primary_key: false) do + add :id, :binary_id, primary_key: true + add :key, :text, null: false + add :value, :text, null: false + add :format, :string, null: false + add :live_data_id, references(:beacon_live_data, on_delete: :delete_all, type: :binary_id), null: false + + timestamps() + end + + create index(:beacon_live_data_assigns, [:live_data_id]) + end +end diff --git a/test/beacon/content_test.exs b/test/beacon/content_test.exs index 30feb83a..5eb5ea48 100644 --- a/test/beacon/content_test.exs +++ b/test/beacon/content_test.exs @@ -9,6 +9,7 @@ defmodule Beacon.ContentTest do alias Beacon.Content.Layout alias Beacon.Content.LayoutEvent alias Beacon.Content.LayoutSnapshot + alias Beacon.Content.LiveData alias Beacon.Content.Page alias Beacon.Content.PageEvent alias Beacon.Content.PageEventHandler @@ -550,4 +551,91 @@ defmodule Beacon.ContentTest do assert {:ok, %Component{body: "new_body"}} = Content.update_component(component, %{body: "new_body"}) end end + + describe "live data" do + test "create_live_data_path/1 OK" do + attrs = %{site: :my_site, path: "/foo/:bar"} + + assert {:ok, %LiveData{} = live_data} = Content.create_live_data(attrs) + assert %{site: :my_site, path: "/foo/:bar"} = live_data + end + + test "get all live data paths for site" do + %{path: live_data_path} = live_data_fixture(site: :my_site) + + assert [^live_data_path] = Content.live_data_paths_for_site(:my_site) + end + + test "search live data paths" do + live_data_fixture(site: :my_site, path: "/foo") + live_data_fixture(site: :my_site, path: "/bar") + + assert ["/foo"] = Content.live_data_paths_for_site(:my_site, query: "fo") + assert ["/bar"] = Content.live_data_paths_for_site(:my_site, query: "ba") + end + + test "get_live_data" do + live_data = live_data_fixture() + + assert Content.get_live_data(live_data.site, live_data.path) == live_data + end + + test "live_data_for_site/1" do + live_data_1 = live_data_fixture(site: :my_site, path: "/foo") + live_data_2 = live_data_fixture(site: :my_site, path: "/bar") + live_data_3 = live_data_fixture(site: :other_site, path: "/baz") + + results = Content.live_data_for_site(:my_site) + + assert Enum.any?(results, &(&1.id == live_data_1.id)) + assert Enum.any?(results, &(&1.id == live_data_2.id)) + refute Enum.any?(results, &(&1.id == live_data_3.id)) + end + + test "update live data" do + live_data = live_data_fixture(site: :my_site, path: "/foo") + + assert {:ok, result} = Content.update_live_data_path(live_data, "/foo/:bar_id") + assert result.id == live_data.id + assert result.path == "/foo/:bar_id" + end + + test "delete live data" do + live_data = live_data_fixture() + + assert [%{}] = Content.live_data_for_site(live_data.site) + assert {:ok, _} = Content.delete_live_data(live_data) + assert [] = Content.live_data_for_site(live_data.site) + end + + test "create_assign_for_live_data/2 OK" do + live_data = live_data_fixture() + attrs = %{key: "product_id", format: :elixir, value: "123"} + + assert {:ok, %LiveData{assigns: [assign]}} = Content.create_assign_for_live_data(live_data, attrs) + assert %{key: "product_id", format: :elixir, value: "123"} = assign + end + + test "update a live data assign" do + live_data = live_data_fixture() + live_data_assign = live_data_assign_fixture(live_data) + + attrs = %{key: "wins", value: "1337", format: :elixir} + assert {:ok, updated_assign} = Content.update_live_data_assign(live_data_assign, attrs) + + assert updated_assign.id == live_data_assign.id + assert updated_assign.key == "wins" + assert updated_assign.value == "1337" + assert updated_assign.format == :elixir + end + + test "delete a live data assign" do + live_data = live_data_fixture() + live_data_assign = live_data_assign_fixture(live_data) + + assert %{assigns: [^live_data_assign]} = Repo.preload(live_data, :assigns) + assert {:ok, _} = Content.delete_live_data_assign(live_data_assign) + assert %{assigns: []} = Repo.preload(live_data, :assigns) + end + end end diff --git a/test/support/fixtures.ex b/test/support/fixtures.ex index 19752b49..bf5684ca 100644 --- a/test/support/fixtures.ex +++ b/test/support/fixtures.ex @@ -2,6 +2,7 @@ defmodule Beacon.Fixtures do alias Beacon.Content alias Beacon.Content.ErrorPage alias Beacon.Content.LiveData + alias Beacon.Content.LiveDataAssign alias Beacon.Content.PageEventHandler alias Beacon.Content.PageVariant alias Beacon.MediaLibrary @@ -204,10 +205,20 @@ defmodule Beacon.Fixtures do def live_data_fixture(attrs \\ %{}) do Repo.insert!(%LiveData{ site: attrs[:site] || :site_a, - path: attrs[:path] || "/foo/bar", - assign: attrs[:assign] || "bar", - format: attrs[:format] || :text, - code: attrs[:code] || "Hello world!" + path: attrs[:path] || "/foo/bar" }) end + + def live_data_assign_fixture(live_data, attrs \\ %{}) do + full_attrs = %{ + key: attrs[:key] || "bar", + value: attrs[:value] || "Hello world!", + format: attrs[:format] || :text + } + + live_data + |> Ecto.build_assoc(:assigns) + |> LiveDataAssign.changeset(full_attrs) + |> Repo.insert!() + end end