Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Shared event handlers #571

Merged
merged 24 commits into from
Aug 24, 2024
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 42 additions & 51 deletions lib/beacon/content.ex
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,14 @@ defmodule Beacon.Content do
alias Beacon.Content.ComponentSlot
alias Beacon.Content.ComponentSlotAttr
alias Beacon.Content.ErrorPage
alias Beacon.Content.EventHandler
alias Beacon.Content.Layout
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
alias Beacon.Content.PageField
alias Beacon.Content.PageSnapshot
alias Beacon.Content.PageVariant
Expand Down Expand Up @@ -702,7 +702,7 @@ defmodule Beacon.Content do

@doc false
def create_page_snapshot(page, event) do
page = repo(page).preload(page, [:variants, :event_handlers])
page = repo(page).preload(page, :variants)

attrs = %{
"site" => page.site,
Expand Down Expand Up @@ -3521,77 +3521,68 @@ defmodule Beacon.Content do

## Example

iex> change_page_event_handler(page_event_handler, %{name: "form-submit"})
%Ecto.Changeset{data: %PageEventHandler{}}
iex> change_event_handler(event_handler, %{name: "form-submit"})
%Ecto.Changeset{data: %EventHandler{}}

"""
@doc type: :page_event_handlers
@spec change_page_event_handler(PageEventHandler.t(), map()) :: Changeset.t()
def change_page_event_handler(%PageEventHandler{} = event_handler, attrs \\ %{}) do
PageEventHandler.changeset(event_handler, attrs)
@doc type: :event_handlers
@spec change_event_handler(EventHandler.t(), map()) :: Changeset.t()
def change_event_handler(%EventHandler{} = event_handler, attrs \\ %{}) do
EventHandler.changeset(event_handler, attrs)
end

@doc """
Creates a new page event handler and returns the page with updated `:event_handlers` association.
Lists all event handlers for a given Beacon site.
"""
@doc type: :page_event_handlers
@spec create_event_handler_for_page(Page.t(), %{name: binary(), code: binary()}) :: {:ok, Page.t()} | {:error, Changeset.t()}
def create_event_handler_for_page(page, attrs) do
changeset =
page
|> Ecto.build_assoc(:event_handlers)
|> PageEventHandler.changeset(attrs)
|> validate_page_event_handler(page)

transact(repo(page), fn ->
with {:ok, %PageEventHandler{}} <- repo(page).insert(changeset),
%Page{} = page <- repo(page).preload(page, :event_handlers, force: true),
%Page{} = page <- Lifecycle.Page.after_update_page(page) do
{:ok, page}
end
end)
@spec list_event_handlers(Site.t()) :: [EventHandler.t()]
def list_event_handlers(site) do
repo(site).all(from eh in EventHandler, where: [site: ^site])
end

@doc """
Updates a page event handler and returns the page with updated `:event_handlers` association.
Creates a new event handler.
"""
@doc type: :page_event_handlers
@spec update_event_handler_for_page(Page.t(), PageEventHandler.t(), map()) :: {:ok, Page.t()} | {:error, Changeset.t()}
def update_event_handler_for_page(page, event_handler, attrs) do
@doc type: :event_handlers
@spec create_event_handler(%{name: binary(), code: binary(), site: Site.t()}) ::
{:ok, EventHandler.t()} | {:error, Changeset.t()}
def create_event_handler(attrs) do
changeset =
event_handler
|> PageEventHandler.changeset(attrs)
|> validate_page_event_handler(page)
%EventHandler{}
|> EventHandler.changeset(attrs)
|> validate_event_handler()

transact(repo(page), fn ->
with {:ok, %PageEventHandler{}} <- repo(page).update(changeset),
%Page{} = page <- repo(page).preload(page, :event_handlers, force: true),
%Page{} = page <- Lifecycle.Page.after_update_page(page) do
{:ok, page}
end
end)
site = Changeset.get_field(changeset, :site)

repo(site).insert(changeset)
end

defp validate_page_event_handler(changeset, page) do
@doc """
Updates an event handler with the given attrs.
"""
@doc type: :event_handlers
@spec update_event_handler(EventHandler.t(), map()) :: {:ok, EventHandler.t()} | {:error, Changeset.t()}
def update_event_handler(event_handler, attrs) do
event_handler
|> EventHandler.changeset(attrs)
|> validate_event_handler()
|> repo(event_handler).update()
end

defp validate_event_handler(changeset) do
code = Changeset.get_field(changeset, :code)
metadata = %Beacon.Template.LoadMetadata{site: page.site, path: page.path}
variable_names = ["socket", "event_params"]
imports = ["Phoenix.Socket"]

do_validate_template(changeset, :code, :elixir, code, metadata, variable_names, imports)
do_validate_template(changeset, :code, :elixir, code, nil, variable_names, imports)
end

@doc """
Deletes a page event handler and returns the page with updated `:event_handlers` association.
Deletes an event handler.
"""
@doc type: :page_event_handlers
@spec delete_event_handler_from_page(Page.t(), PageEventHandler.t()) :: {:ok, Page.t()} | {:error, Changeset.t()}
def delete_event_handler_from_page(page, event_handler) do
with {:ok, %PageEventHandler{}} <- repo(page).delete(event_handler),
%Page{} = page <- repo(page).preload(page, :event_handlers, force: true),
%Page{} = page <- Lifecycle.Page.after_update_page(page) do
{:ok, page}
end
@doc type: :event_handlers
@spec delete_event_handler(EventHandler.t()) :: {:ok, EventHandler.t()} | {:error, Changeset.t()}
def delete_event_handler(event_handler) do
repo(event_handler).delete(event_handler)
end

# PAGE VARIANTS
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
defmodule Beacon.Content.PageEventHandler do
defmodule Beacon.Content.EventHandler do
@moduledoc """
Beacon's representation of a LiveView [handle_event/3](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#c:handle_event/3).

Expand All @@ -17,31 +17,29 @@ defmodule Beacon.Content.PageEventHandler do

import Ecto.Changeset

alias Beacon.Content.Page
alias Beacon.Types.Site
alias Ecto.UUID

@type t :: %__MODULE__{
id: UUID.t(),
name: binary(),
code: binary(),
page_id: UUID.t(),
page: Page.t(),
site: Site.t(),
inserted_at: DateTime.t(),
updated_at: DateTime.t()
}

schema "beacon_page_event_handlers" do
schema "beacon_event_handlers" do
field :name, :string
field :code, :string

belongs_to :page, Page
field :site, Site

timestamps()
end

@doc false
def changeset(%__MODULE__{} = event_handler, attrs) do
fields = ~w(name code)a
fields = ~w(name code site)a

event_handler
|> cast(attrs, fields)
Expand Down
2 changes: 1 addition & 1 deletion lib/beacon/content/live_data_assign.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
defmodule Beacon.Content.LiveDataAssign do
@moduledoc """
Dynamic key/value assigns to be used by `Beacon.Content.Page` templates and updated with `Beacon.Content.PageEventHandler`s.
Dynamic key/value assigns to be used by `Beacon.Content.Page` templates and updated with `Beacon.Content.EventHandler`s.

LiveDataAssigns don't exist on their own, but exist as part of a `Beacon.Content.LiveData` struct.

Expand Down
1 change: 0 additions & 1 deletion lib/beacon/content/page.ex
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ defmodule Beacon.Content.Page do
belongs_to :layout, Content.Layout

has_many :variants, Content.PageVariant
has_many :event_handlers, Content.PageEventHandler

embeds_many :helpers, Helper

Expand Down
11 changes: 6 additions & 5 deletions lib/beacon/loader/page.ex
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
defmodule Beacon.Loader.Page do
@moduledoc false

require Logger
alias Beacon.Content
alias Beacon.Lifecycle
alias Beacon.Loader
alias Beacon.Template.HEEx

require Logger

def module_name(site, page_id), do: Loader.module_name(site, "Page#{page_id}")

def build_ast(site, page) do
Expand Down Expand Up @@ -96,7 +97,7 @@ defmodule Beacon.Loader.Page do

defp interpolate_raw_schema_record(schema, page) when is_map(schema) do
render = fn key, value, page ->
case Beacon.Content.render_snippet(value, %{page: page, live_data: %{}}) do
case Content.render_snippet(value, %{page: page, live_data: %{}}) do
{:ok, new_value} ->
{key, new_value}

Expand Down Expand Up @@ -124,10 +125,10 @@ defmodule Beacon.Loader.Page do
end

defp handle_event(page) do
%{site: site, event_handlers: event_handlers} = page
event_handlers = Content.list_event_handlers(page.site)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's still one step missing here :)

Whenever an event handler changes we'd need to broadcast a message to recompile all page modules so I'm wondering if it would be better to create a new EventHandler module containing all handle_event and on the handle_event of PageLive here

result =
Beacon.apply_mfa(
page_module,
:handle_event,
[event_name, event_params, socket],
context: %{site: site, page_id: page_id, live_path: live_path}
)

We just replace page_module with event_handler_module.

In this case whenever an event handler changes we only recompile the single event handler module and save resources by not duplicating the same event_handler on all page modules.

Wdyt?


Enum.map(event_handlers, fn event_handler ->
Beacon.safe_code_check!(site, event_handler.code)
Beacon.safe_code_check!(page.site, event_handler.code)

quote do
def handle_event(unquote(event_handler.name), var!(event_params), var!(socket)) do
Expand Down
94 changes: 80 additions & 14 deletions lib/beacon/migration.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ defmodule Beacon.Migration do
To install Beacon, you'll need to generate an `Ecto.Migration` that wraps calls to `Beacon.Migration`:

```
mix ecto.gen.migration create_beacon_tables
$ mix ecto.gen.migration create_beacon_tables
```

Open the generated migration in your editor and either call or delegate to `up/0` and `down/0`:
Open the generated migration in your editor and either call or delegate to `up/1` and `down/1`:

```elixir
defmodule MyApp.Repo.Migrations.CreateBeaconTables do
Expand All @@ -23,30 +23,96 @@ defmodule Beacon.Migration do
Then, run the migrations for your app to create the necessary Beacon tables in your database:

```
mix ecto.migrate
$ mix ecto.migrate
```

Note that `up/0` will always execute all migration steps from the initial version to the latest version,
and those migration are idempotent.
By calling `up()` with no arguments, this will execute all migration steps from the initial version to
the latest version. As new versions are released, you may need to repeat this process, by first
generating a new migration:

Check out the [your first site](https://hexdocs.pm/beacon/your-first-site.html) guide for a full example.
```
$ mix ecto.gen.migration upgrade_beacon_tables_to_v2
```

Then in the generated migration, you could simply call `up()` again, because the migrations are
idempotent, but you can be safer and more efficient by specifying the migration version to execute:

```elixir
defmodule MyApp.Repo.Migrations.UpgradeBeaconTables do
use Ecto.Migration
def up, do: Beacon.Migration.up(version: 2)
def down, do: Beacon.Migration.down(version: 2)
end
```

Now this migration will update to v2, but if rolled back, will only roll back the v2 changes,
leaving v1 tables in-place.

To see this step within the larger context of installing Beacon, check out the [your first site](your-first-site.html) guide.
"""

# TODO: `up/1` should execute all migrations from v001 up to `@latest`
@latest Beacon.Migrations.V001
@initial_version 1
@current_version 2

@doc """
Run the `up` changes for all migrations between the initial version and the current version.
Upgrades Beacon database schemas.

If a specific version number is provided, Beacon will only upgrade to that version.
Otherwise, it will bring you fully up-to-date with the current version.

## Example

Run all migrations up to the current version:

Beacon.Migration.up()

Run migrations up to a specified version:

Beacon.Migration.down(version: 2)

"""
def up do
@latest.up()
def up(opts \\ []) do
versions_to_run =
case opts[:version] do
nil -> @initial_version..@current_version//1
version -> @initial_version..version//1
end

Enum.each(versions_to_run, fn version ->
padded = String.pad_leading("#{version}", 3, "0")
module = Module.concat([Beacon.Migrations, "V#{padded}"])
module.up()
end)
end

@doc """
Run the `down` changes for all migrations between the initial version and the current version.
Downgrades Beacon database schemas.

If a specific version number is provided, Beacon will only downgrade to that version (inclusive).
Otherwise, it will completely uninstall Beacon from your app's database.

## Example

Run all migrations from current version down to the first:

Beacon.Migration.down()

Run migrations down to and including a specified version:

Beacon.Migration.down(version: 2)

"""
def down do
@latest.down()
def down(opts \\ []) do
versions_to_run =
case opts[:version] do
nil -> @current_version..@initial_version//-1
version -> @current_version..version//-1
end

Enum.each(versions_to_run, fn version ->
padded = String.pad_leading("#{version}", 3, "0")
module = Module.concat([Beacon.Migrations, "V#{padded}"])
module.down()
end)
end
end
Loading
Loading