Skip to content

Instantly share code, notes, and snippets.

@nallwhy
Last active July 26, 2024 09:21
Show Gist options
  • Save nallwhy/77f6fee12d919c2513f0ada2487d6e22 to your computer and use it in GitHub Desktop.
Save nallwhy/77f6fee12d919c2513f0ada2487d6e22 to your computer and use it in GitHub Desktop.
ash policy example
Mix.install(
[
{:plug_cowboy, "~> 2.7"},
{:jason, "~> 1.0"},
{:phoenix, "~> 1.7.0"},
{:phoenix_live_view, "~> 0.20.0"},
{:ash, "3.2.6"},
{:ash_phoenix, "2.0.4"},
{:ash_state_machine, "0.2.5"},
{:simple_sat, "~> 0.1"}
],
config: [
sample: [
{SamplePhoenix.Endpoint,
[
http: [ip: {127, 0, 0, 1}, port: 5001],
server: true,
live_view: [signing_salt: "aaaaaaaa"],
secret_key_base: String.duplicate("a", 64)
]}
],
ash: [require_atomic_by_default?: false]
]
)
defmodule Room do
use Ash.Resource,
data_layer: Ash.DataLayer.Ets,
domain: Domain,
authorizers: [Ash.Policy.Authorizer],
extensions: [AshStateMachine]
attributes do
uuid_primary_key :id
attribute :name, :string, allow_nil?: false
attribute :state, :atom,
allow_nil?: false,
default: :unsold,
constraints: [one_of: [:unsold, :contracted]]
end
relationships do
has_one :contract, Contract
end
state_machine do
initial_states [:unsold]
default_initial_state :unsold
transitions do
transition :contract, from: :unsold, to: :contracted
end
end
actions do
defaults create: [:name, :state]
update :contract do
# should pass params to contract
argument :contract_params, :map, allow_nil?: false
change transition_state(:contracted)
change manage_relationship(:contract_params, :contract, type: :create)
end
end
code_interface do
define :create
define :contract
end
policies do
policy action(:contract) do
forbid_unless AshStateMachine.Checks.ValidNextState
authorize_if matches("can create contract", fn actor, _context ->
Ash.can?({Contract, :create}, actor)
end)
end
policy always() do
authorize_if always()
end
end
end
defmodule Contract do
use Ash.Resource,
data_layer: Ash.DataLayer.Ets,
domain: Domain,
authorizers: [Ash.Policy.Authorizer]
attributes do
uuid_primary_key :id
attribute :date, :date, allow_nil?: false
end
relationships do
belongs_to :room, Room, allow_nil?: false
end
actions do
defaults create: [:room_id, :date]
end
code_interface do
define :create
end
policies do
policy action(:create) do
authorize_if always()
end
end
end
defmodule Domain do
use Ash.Domain
resources do
resource Room
resource Contract
end
end
defmodule SamplePhoenix.ErrorView do
def render(template, _), do: Phoenix.Controller.status_message_from_template(template)
end
defmodule SamplePhoenix.SampleLive do
use Phoenix.LiveView, layout: {__MODULE__, :live}
alias Phoenix.LiveView.JS
def mount(_params, _session, socket) do
room0 = Room.create!(%{name: "room0"})
room1 =
Room.create!(%{name: "room1"})
|> Room.contract!(%{contract_params: %{date: Date.utc_today()}})
rooms = [room0, room1]
socket =
socket
|> assign(rooms: rooms)
|> assign(form: nil)
{:ok, socket}
end
# layout
def render("live.html", assigns) do
~H"""
<script src="https://cdn.jsdelivr.net/npm/phoenix@1.7.0-rc.2/priv/static/phoenix.min.js">
</script>
<script
src="https://cdn.jsdelivr.net/npm/phoenix_live_view@0.18.2/priv/static/phoenix_live_view.min.js"
>
</script>
<script>
let liveSocket = new window.LiveView.LiveSocket("/live", window.Phoenix.Socket)
liveSocket.connect()
</script>
<style>
* { font-size: 1.1em; }
</style>
<%= @inner_content %>
"""
end
# render
def render(assigns) do
~H"""
<div>
<table>
<thead>
<th>name</th>
<th>state</th>
<th>action</th>
</thead>
<tbody>
<tr :for={room <- @rooms}>
<td><%= room.name %></td>
<td><%= room.state %></td>
<td>
<button
:if={can_contract?(room)}
phx-click={JS.push("assign_contract_form", value: %{room_id: room.id})}
>
Contract
</button>
</td>
</tr>
</tbody>
</table>
</div>
<div>
<.form :if={@form} for={@form} phx-change="validate">
<.inputs_for :let={c} field={@form[:contract_params]}>
<input type="date" id={c[:date].id} name={c[:date].name} />
</.inputs_for>
<button>Save</button>
</.form>
</div>
"""
end
@impl true
def handle_event("assign_contract_form", %{"room_id" => room_id}, socket) do
room = socket.assigns.rooms |> Enum.find(&(&1.id == room_id))
form =
room
|> AshPhoenix.Form.for_update(:contract, forms: [auto?: true])
|> to_form()
|> AshPhoenix.Form.add_form([:contract_params])
socket =
socket
|> assign(form: form)
{:noreply, socket}
end
@impl true
def handle_event("validate", %{"form" => params}, socket) do
form = AshPhoenix.Form.validate(params)
socket =
socket
|> assign(form: form)
{:noreply, socket}
end
defp can_contract?(room) do
Ash.can?({room, :contract}, nil)
end
end
defmodule Router do
use Phoenix.Router
import Phoenix.LiveView.Router
pipeline :browser do
plug(:accepts, ["html"])
end
scope "/", SamplePhoenix do
pipe_through(:browser)
live("/", SampleLive, :index)
end
end
defmodule SamplePhoenix.Endpoint do
use Phoenix.Endpoint, otp_app: :sample
socket("/live", Phoenix.LiveView.Socket)
plug(Router)
end
{:ok, _} = Supervisor.start_link([SamplePhoenix.Endpoint], strategy: :one_for_one)
Process.sleep(:infinity)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment