Mix.install([
{:ecto_sql, "~> 3.8.0"},
{:ecto_sqlite3, "~> 0.8.0"},
{:phoenix, "~> 1.6.11"},
{:phoenix_live_view, "~> 0.17.11"},
{:phoenix_ecto, "~> 4.4"},
{:jason, "~> 1.3"},
{:plug_cowboy, "~> 2.5"}
])
Application.put_env(:phoenix, :json_library, Jason)
Application.put_env(:foo, Repo,
database: Path.expand(Path.join(__DIR__, "test.db")),
pool: Ecto.Adapters.SQL.Sandbox
)
Application.put_env(:foo, :ecto_repos, [Repo])
Application.put_env(:foo, Foo.Endpoint,
server: true,
live_view: [signing_salt: "aaaaaaaa"],
http: [
# Enable IPv6 and bind on all interfaces.
# Set it to {0, 0, 0, 0, 0, 0, 0, 1} for local network only access.
ip: {0, 0, 0, 0, 0, 0, 0, 0},
port: 8001
],
secret_key_base: :crypto.strong_rand_bytes(32) |> Base.encode32()
)
This Livebook sets up a sandboxed LiveView environment for exploring an issue with Tasks: phoenixframework/phoenix_ecto#157
Adapted from: https://github.com/wojtekmach/mix_install_examples/blob/main/ecto_sql.exs
defmodule Repo do
use Ecto.Repo, otp_app: :foo, adapter: Ecto.Adapters.SQLite3
end
defmodule Migration0 do
use Ecto.Migration
def change do
create table("posts") do
add(:title, :string)
timestamps(type: :utc_datetime_usec)
end
end
end
defmodule Post do
use Ecto.Schema
schema "posts" do
field(:title, :string)
timestamps(type: :utc_datetime_usec)
end
end
Adapted from: https://github.com/wojtekmach/mix_install_examples/blob/main/phoenix_live_view.exs
defmodule Foo do
defmodule Router do
use Phoenix.Router
import Phoenix.LiveView.Router
import Plug.Conn
import Phoenix.Controller
pipeline :browser do
plug(:accepts, ["html"])
end
scope "/", Foo do
pipe_through([:browser])
live("/sample", SampleLive, :index)
live("/sandbox", SandboxLive, :index)
end
end
defmodule Endpoint do
use Phoenix.Endpoint, otp_app: :foo
plug(Phoenix.Ecto.SQL.Sandbox)
@session_options [
store: :cookie,
key: "_example_key",
signing_salt: "1234"
]
plug(Plug.Parsers,
parsers: [:urlencoded, :multipart, :json],
pass: ["*/*"],
json_decoder: Jason
)
plug(Plug.Session, @session_options)
socket("/live", Phoenix.LiveView.Socket)
plug(Foo.Router)
end
defmodule ErrorView do
use Phoenix.View,
root: "does/not/matter",
namespace: Foo
def render("404.html", _assigns) do
"Not found"
end
def render("500.html", _assigns) do
"500 - ooooops!"
end
end
end
defmodule Foo.SampleLive do
use Phoenix.LiveView, layout: {__MODULE__, "live.html"}
@impl true
def mount(_params, _session, socket) do
:ok = Ecto.Adapters.SQL.Sandbox.checkout(Repo)
metadata = Phoenix.Ecto.SQL.Sandbox.metadata_for(Repo, self())
{:ok, assign(socket, :metadata, Phoenix.Ecto.SQL.Sandbox.encode_metadata(metadata))}
end
def render("live.html", assigns) do
~H"""
<style>
html, body {
margin: 0;
padding: 0;
}
</style>
<script src="https://cdn.jsdelivr.net/npm/phoenix@1.6.11/priv/static/phoenix.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/phoenix_live_view@0.17.11/priv/static/phoenix_live_view.min.js"></script>
<script>
let liveSocket = new window.LiveView.LiveSocket("/live", window.Phoenix.Socket);
liveSocket.connect();
</script>
<%= @inner_content %>
"""
end
@impl true
def render(assigns) do
# sandboxed iframe
~H"""
<iframe
src={Foo.Router.Helpers.sandbox_path(@socket, :index, %{"sandbox" => @metadata})}
style="width: 100%; height: 100%; border: none;"
></iframe>
"""
end
end
defmodule Foo.SandboxLive do
use Phoenix.LiveView, layout: {Foo.SampleLive, "live.html"}
@impl true
def mount(%{"sandbox" => metadata}, _session, socket) do
Phoenix.Ecto.SQL.Sandbox.allow(metadata, Ecto.Adapters.SQL.Sandbox)
{:ok, assign(socket, :data, nil)}
end
@impl true
def handle_event("load", _, socket) do
data = Repo.all(Post)
{:noreply, assign(socket, :data, data)}
end
def handle_event("load-async", _, socket) do
target = self()
Task.start(fn ->
data = Repo.all(Post)
send(target, {:data, data})
end)
{:noreply, assign(socket, :data, :loading)}
end
def handle_event("load-async-allow", _, socket) do
target = self()
{:ok, pid} =
Task.start(fn ->
receive do
:ok -> :ok
end
data = Repo.all(Post)
send(target, {:data, data})
end)
Ecto.Adapters.SQL.Sandbox.allow(Repo, self(), pid)
send(pid, :ok)
{:noreply, assign(socket, :data, :loading)}
end
def handle_event("create-post", _params, socket) do
Repo.insert!(%Post{title: "Hello, World!"})
{:noreply, socket}
end
@impl true
def handle_info({:data, data}, socket) do
{:noreply, assign(socket, :data, data)}
end
@impl true
def render(assigns) do
~H"""
<div style="padding: 4px 16px; margin: 0 auto; max-width: 800px;">
<h1>Demo</h1>
<p>
Everything that happens on this page is sandboxed.
You can create a new sandbox by reloading the page.
</p>
<hr>
<p>
Insert a new post into the database by pressing the "New Post" button.
Next, either load the data synchronously inside the LiveView, or asynchronously
using a Task. The task does not use the sandbox and therefore does not return the
created post.
</p>
<button phx-click="load">Load Data (Sync)</button>
<button phx-click="load-async">Load Data (Task)</button>
<button phx-click="load-async-allow">Load Data (Allowed Task)</button>
<button phx-click="create-post">New Post</button>
<pre style="background: #fafafa; border-radius: 5px; border: gray;">
<%= inspect(@data, pretty: true) %>
</pre>
</div>
"""
end
end
defmodule Main do
def main do
children = [
Repo,
Foo.Endpoint
]
_ = Repo.__adapter__().storage_down(Repo.config())
:ok = Repo.__adapter__().storage_up(Repo.config())
{:ok, _} = Supervisor.start_link(children, strategy: :one_for_one)
end
end
{:ok, main_pid} = Main.main()
Ecto.Migrator.run(Repo, [{0, Migration0}], :up, all: true, log_migrations_sql: :debug)
When everything is running, visit http://localhost:8001/sample