Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Ash Framework - Elixir Kenya February 2024 Webinar

Ash Framework - Elixir Kenya February 2024 Webinar

A very quick introduction into the basics of the Ash Framework.

Stefan Wintermeyer

February 23, 2024
Tweet

More Decks by Stefan Wintermeyer

Other Decks in Programming

Transcript

  1. „… when the various components of your system can have

    consistent expectations of how the other components around them work, you can ultimately do a significant amount more, with less.“ https://ash-hq.org/docs/guides/ash/latest/tutorials/why-ash
  2. defmodule App.ToDoList.Task do use Ash.Resource, data_layer: AshPostgres.DataLayer postgres do table

    "tasks" repo App.Repo end attributes do uuid_primary_key :id attribute :content, :string end end
  3. $ mix ash_postgres.create The database for App.Repo has been created

    $ mix ash_postgres.migrate Compiling 1 file (.ex) 09:51:39.227 [info] == Running 20240221085028 App.Repo.Migrations.MigrateResources1.up/0 forward 09:51:39.228 [info] create table tasks 09:51:39.232 [info] == Migrated 20240221085028 in 0.0s
  4. defmodule App.ToDoList.Task do use Ash.Resource, data_layer: AshPostgres.DataLayer postgres do table

    "tasks" repo App.Repo end attributes do uuid_primary_key :id attribute :content, :string, allow_nil?: false attribute :position, :integer, allow_nil?: false end end
  5. $ mix ash_postgres.generate_migrations --name add_position Compiling 1 file (.ex) Extension

    Migrations: No extensions to install Generating Tenant Migrations: Generating Migrations: * creating priv/repo/migrations/20240221113258_add_position.exs $ mix ash_postgres.migrate 12:33:29.451 [info] == Running 20240221113258 App.Repo.Migrations.AddPosition.up/0 forward 12:33:29.452 [info] alter table tasks 12:33:29.455 [info] == Migrated 20240221113258 in 0.0s
  6. iex(1)> App.ToDoList.Task.create!(%{content: "Mow the lawn", position: 1}) [debug] QUERY OK

    db=2.4ms idle=129.0ms begin [] ↳ anonymous fn/3 in Ash.Changeset.with_hooks/3, at: lib/ash/changeset/ changeset.ex:2404 [debug] QUERY OK source="tasks" db=2.2ms INSERT INTO "tasks" ("id","position","content") VALUES ($1,$2,$3) RETURNING "position","content","id" ["09380ab1-a761-4281-bf24-41e11973fdd2", 1, "Mow the lawn"] ↳ AshPostgres.DataLayer.bulk_create/3, at: lib/data_layer.ex:1582 [debug] QUERY OK db=1.1ms commit [] ↳ anonymous fn/3 in Ash.Changeset.with_hooks/3, at: lib/ash/changeset/ changeset.ex:2404 #App.ToDoList.Task< __meta__: #Ecto.Schema.Metadata<:loaded, "tasks">, id: "09380ab1-a761-4281-bf24-41e11973fdd2", content: "Mow the lawn", position: 1, aggregates: %{}, calculations: %{}, ... >
  7. iex(2)> App.ToDoList.Task.read [debug] QUERY OK source="tasks" db=0.7ms queue=1.0ms idle=1138.9ms SELECT

    t0."id", t0."content", t0."position" FROM "tasks" AS t0 [] ↳ anonymous fn/3 in AshPostgres.DataLayer.run_query/2, at: lib/ data_layer.ex:686 {:ok, [ #App.ToDoList.Task< __meta__: #Ecto.Schema.Metadata<:loaded, "tasks">, id: "09380ab1-a761-4281-bf24-41e11973fdd2", content: "Mow the lawn", position: 1, aggregates: %{}, calculations: %{}, ... > ]}
  8. iex(3)> App.ToDoList.Task.by_id!("09380ab1-a761-4281- bf24-41e11973fdd2") [debug] QUERY OK source="tasks" db=0.6ms queue=0.8ms idle=1350.3ms

    SELECT t0."id", t0."content", t0."position" FROM "tasks" AS t0 WHERE (t0."id"::uuid = $1::uuid) ["09380ab1-a761-4281- bf24-41e11973fdd2"] ↳ anonymous fn/3 in AshPostgres.DataLayer.run_query/2, at: lib/ data_layer.ex:686 #App.ToDoList.Task< __meta__: #Ecto.Schema.Metadata<:loaded, "tasks">, id: "09380ab1-a761-4281-bf24-41e11973fdd2", content: "Mow the lawn", position: 1, aggregates: %{}, calculations: %{}, ... >
  9. iex(4)> {:ok, tasks} = App.ToDoList.Task.read {:ok, [ #App.ToDoList.Task< __meta__: #Ecto.Schema.Metadata<:loaded,

    "tasks">, id: "09380ab1-a761-4281-bf24-41e11973fdd2", content: "Mow the lawn", ... > ]} iex(5)> tasks = App.ToDoList.Task.read! [ #App.ToDoList.Task< __meta__: #Ecto.Schema.Metadata<:loaded, "tasks">, id: "09380ab1-a761-4281-bf24-41e11973fdd2", content: "Mow the lawn", ... > ]
  10. +----------+ +-------------+ | Category | | Product | +----------+ +-------------+

    | id |<-----| category_id | | name | | id | | | | name | | | | price | +----------+ +-------------+ belongs_to
  11. defmodule App.Shop.Category do use Ash.Resource, data_layer: Ash.DataLayer.Ets attributes do uuid_primary_key

    :id attribute :name, :string end actions do defaults [:create, :read] end code_interface do define_for App.Shop define :create define :read end end
  12. defmodule App.Shop.Product do use Ash.Resource, data_layer: Ash.DataLayer.Ets attributes do uuid_primary_key

    :id attribute :name, :string attribute :price, :decimal end relationships do belongs_to :category, App.Shop.Category do attribute_writable? true end end # ...
  13. $ iex -S mix iex(1)> alias App.Shop.Product App.Shop.Product iex(2)> alias

    App.Shop.Category App.Shop.Category iex(3)> fruits = Category.create!(%{name: "Fruits"}) #App.Shop.Category< __meta__: #Ecto.Schema.Metadata<:loaded>, id: "91cb42d8-45c2-451d-8261-72ae4d94a3c6", name: "Fruits", ...
  14. iex(4)> orange = Product.create!(%{ name: "Orange", price: 0.15, category_id: fruits.id

    }) #App.Shop.Product< category: #Ash.NotLoaded<:relationship>, __meta__: #Ecto.Schema.Metadata<:loaded>, id: "6870b44b-67ed-4186-97ed-bbfffd1fc2a0", name: "Orange", price: Decimal.new("0.15"), category_id: "91cb42d8-45c2-451d-8261-72ae4d94a3c6", ... >
  15. iex(5)> App.Shop.load(orange, :category) {:ok, #App.Shop.Product< category: #App.Shop.Category< __meta__: #Ecto.Schema.Metadata<:loaded>, id:

    "91cb42d8-45c2-451d-8261-72ae4d94a3c6", name: "Fruits", ... >, __meta__: #Ecto.Schema.Metadata<:loaded>, id: "6870b44b-67ed-4186-97ed-bbfffd1fc2a0", name: "Orange", price: Decimal.new("0.15"), category_id: "91cb42d8-45c2-451d-8261-72ae4d94a3c6", ... >}
  16. iex(6)> orange2 = Product.by_name!("Orange", load: [:category]) #App.Shop.Product< category: #App.Shop.Category< __meta__:

    #Ecto.Schema.Metadata<:loaded>, id: "91cb42d8-45c2-451d-8261-72ae4d94a3c6", name: "Fruits", ... >, __meta__: #Ecto.Schema.Metadata<:loaded>, id: "6870b44b-67ed-4186-97ed-bbfffd1fc2a0", name: "Orange", price: Decimal.new("0.15"), category_id: "91cb42d8-45c2-451d-8261-72ae4d94a3c6", ... >
  17. $ mix ash_phoenix.gen.html App.ToDoList App.ToDoList.Task * creating lib/app_web/controllers/task_controller.ex * creating

    lib/app_web/controllers/task_html/edit.html.heex * creating lib/app_web/controllers/task_html.ex * creating lib/app_web/controllers/task_html/index.html.heex * creating lib/app_web/controllers/task_html/new.html.heex * creating lib/app_web/controllers/task_html/ task_form.html.heex * creating lib/app_web/controllers/task_html/show.html.heex Add the resource to your browser scope in lib/task_web/ router.ex: resources "/tasks", TaskController
  18. defmodule AppWeb.TaskController do use AppWeb, :controller alias App.ToDoList.Task def index(conn,

    _params) do tasks = Task.read!() render(conn, :index, tasks: tasks) end def new(conn, _params) do render(conn, :new, form: create_form()) end # …
  19. <.header> Task <%= @task.id %> <:subtitle>This is a task record

    from your database.</:subtitle> <:actions> <.link href={~p"/tasks/#{@task}/edit"}> <.button>Edit task</.button> </.link> </:actions> </.header> <.list> <:item title="Content"><%= @task.content %></:item> <:item title="Position"><%= @task.position %></:item> </.list> <.back navigate={~p"/tasks"}>Back to tasks</.back>
  20. iex(4)> App.ToDoList.Task.create!(%{}) ** (Ash.Error.Invalid) Input Invalid * attribute content is

    required (elixir 1.16.0) lib/process.ex:860: Process.info/2 (ash 2.19.3) lib/ash/error/exception.ex:59: Ash.Error.Changes.Required.exception/1 (ash 2.19.3) lib/ash/changeset/changeset.ex:2199: anonymous fn/2 in Ash.Changeset.require_values/4 […]
  21. defmodule AppWeb.Api.Router do use AshJsonApi.Api.Router, # The api modules you

    want to serve apis: [App.ToDoList], # optionally a json_schema route json_schema: "/json_schema", # optionally an open_api route open_api: "/open_api" end
  22. defmodule App.ToDoList.Task do use Ash.Resource, data_layer: AshPostgres.DataLayer, extensions: [AshJsonApi.Resource] #

    ... json_api do type "task" routes do base("/tasks") get(:read) index(:read) post(:create) patch(:update) end end end
  23. $ curl http://localhost:4000/api/json/tasks {"data":[{"attributes":{"position":1,"content":"Mow the lawn"},"id":"09380ab1-a761-4281-bf24-41e11973fdd2","links": {},"meta":{},"type":"task","relationships":{}}],"links": {"self":"http://localhost:4000"},"meta":{},"jsonapi": {"version":"1.0"}} $

    curl http://localhost:4000/api/json/tasks/09380ab1- a761-4281-bf24-41e11973fdd2 {"data":{"attributes":{"position":1,"content":"Mow the lawn"},"id":"09380ab1-a761-4281-bf24-41e11973fdd2","links": {},"meta":{},"type":"task","relationships":{}},"links": {"self":"http://localhost:4000"},"jsonapi":{"version":"1.0"}}
  24. graphql do type :task queries do get :get_task, :read list

    :list_tasks, :read end mutations do create :create_task, :create update :update_task, :update destroy :destroy_task, :destroy end end
  25. defmodule App.ToDoList.User do use Ash.Resource, data_layer: AshPostgres.DataLayer attributes do uuid_primary_key

    :id attribute :first_name, :string, allow_nil?: false attribute :last_name, :string, allow_nil?: false end calculations do calculate :full_name, :string, expr(first_name <> " " <> last_name) end preparations do prepare build(load: [:full_name]) end # …
  26. iex(4)> App.ToDoList.User.read! [debug] QUERY OK source="users" db=0.6ms queue=0.9ms idle=1655.9ms SELECT

    u0."id", u0."first_name", u0."last_name", (u0."first_name"::text::varchar || ($1::varchar || u0."last_name"::text::varchar)::varchar)::text::text FROM "users" AS u0 [" "] ↳ anonymous fn/3 in AshPostgres.DataLayer.run_query/2, at: lib/ data_layer.ex:686 [ #App.ToDoList.User< full_name: "Stefan Wintermeyer", __meta__: #Ecto.Schema.Metadata<:loaded, "users">, id: "2290b296-6181-4197-99f9-e8b40c60ce67", first_name: "Stefan", last_name: "Wintermeyer", aggregates: %{}, calculations: %{},
  27. policies do bypass actor_attribute_equals(:super_user, true) do authorize_if always() end policy

    action_type(:read) do # unless the actor is an active user, forbid forbid_unless actor_attribute_equals(:active, true) # if the record is marked as public, authorize authorize_if attribute(:public, true) # if the actor is related to the data via that data's `owner` relationship, authorize authorize_if relates_to_actor_via(:owner) end end