Last active
July 26, 2024 09:21
-
-
Save nallwhy/77f6fee12d919c2513f0ada2487d6e22 to your computer and use it in GitHub Desktop.
ash policy example
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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/[email protected]/priv/static/phoenix.min.js"> | |
</script> | |
<script | |
src="https://cdn.jsdelivr.net/npm/[email protected]/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