Robert Graff
Robert Graff
AEAsh Elixir
Created by Robert Graff on 8/18/2023 in #support
Duplicate input types for GraphQL
Given a resource like this
defmodule Package
attributes do
attribute :options, {:array, Option}, allow_nil?: false
end

graphql do
type :package
mutations do
create :create
update :add_option, :add_option, identity: :key
end
end

actions do
defaults [:create]

update :add_option do
accept []
argument :option, Option, allow_nil?: false
...
end
end
defmodule Package
attributes do
attribute :options, {:array, Option}, allow_nil?: false
end

graphql do
type :package
mutations do
create :create
update :add_option, :add_option, identity: :key
end
end

actions do
defaults [:create]

update :add_option do
accept []
argument :option, Option, allow_nil?: false
...
end
end
I get multiple errors about duplicate related to Option (UnionType) and its children (OptionString, OptionBoolean).
Type name "OptionString" is not unique.

References to types must be unique.
Type name "OptionString" is not unique.

References to types must be unique.
If I comment out the update mutation, it generates all the types as expected for the create. The update mutation should not require any additional type or input types.
11 replies
AEAsh Elixir
Created by Robert Graff on 8/11/2023 in #support
Applying constraints to embedded union types
I have a resource when an embedded array of union types:
defmodule MyApi.Package
attributes do
...
attribute :options, {:array, PackageOption}, allow_nil?: false
end
end
defmodule MyApi.Package
attributes do
...
attribute :options, {:array, PackageOption}, allow_nil?: false
end
end
defmodule MyApi.PackageOption do
use Ash.Type.NewType,
subtype_of: :union,
constraints: [
types: [
boolean: [
type: MyApi.PackageOption.Boolean
tag: :type,
tag_value: :boolean
],
string: [
type: MyApi.PackageOption.String
tag: :type,
tag_value: :string
]
]
]
end

defmodule MyApi.PackageOption.Boolean do
use Ash.Resource, data_layer: :embedded

attributes do
uuid_primary_key :id
attribute :key, :ci_string, allow_nil?: false, constraints: [match: ~r/^[a-z0-9-]+$/]
attribute :value, :boolean
attribute :type, :atom, constraints: [one_of: [:boolean]]
attribute :enabled, :boolean, allow_nil?: false
end

identities do
identity :key, [:key]
end
end

defmodule MyApi.PackageOption.String do
use Ash.Resource, data_layer: :embedded

attributes do
uuid_primary_key :id
attribute :key, :ci_string, allow_nil?: false, constraints: [match: ~r/^[a-z0-9-]+$/]
attribute :value, :text
attribute :type, :atom, constraints: [one_of: [:string]]
attribute :enabled, :boolean, allow_nil?: false
end

identities do
identity :key, [:key]
end
end
defmodule MyApi.PackageOption do
use Ash.Type.NewType,
subtype_of: :union,
constraints: [
types: [
boolean: [
type: MyApi.PackageOption.Boolean
tag: :type,
tag_value: :boolean
],
string: [
type: MyApi.PackageOption.String
tag: :type,
tag_value: :string
]
]
]
end

defmodule MyApi.PackageOption.Boolean do
use Ash.Resource, data_layer: :embedded

attributes do
uuid_primary_key :id
attribute :key, :ci_string, allow_nil?: false, constraints: [match: ~r/^[a-z0-9-]+$/]
attribute :value, :boolean
attribute :type, :atom, constraints: [one_of: [:boolean]]
attribute :enabled, :boolean, allow_nil?: false
end

identities do
identity :key, [:key]
end
end

defmodule MyApi.PackageOption.String do
use Ash.Resource, data_layer: :embedded

attributes do
uuid_primary_key :id
attribute :key, :ci_string, allow_nil?: false, constraints: [match: ~r/^[a-z0-9-]+$/]
attribute :value, :text
attribute :type, :atom, constraints: [one_of: [:string]]
attribute :enabled, :boolean, allow_nil?: false
end

identities do
identity :key, [:key]
end
end
When doing a create or update on the parent (MyApi.Package) with an option that has an invalid key (see constraint), it's not returning %Ash.Error.Invalid{} as expected. I've tried applying a validation in lieu of an attribute constraint, but the validation is never called. Additionally, the identity is not enforced.
22 replies
AEAsh Elixir
Created by Robert Graff on 8/3/2023 in #support
Why don't read actions filter by attributes?
Given a read action
code_interface do
define :read
end

actions do
defaults [:read]
end
code_interface do
define :read
end

actions do
defaults [:read]
end
There are no arguments for the action, so the following does not select on email.
iex(21)> Membership.read(%{email: "[email protected]"}, authorize?: false)
[debug] QUERY OK source="memberships" db=0.6ms queue=0.1ms idle=1341.0ms
SELECT m0."id", m0."role", m0."email", m0."invitation_token", m0."invited_at", m0."inserted_at", m0."updated_at", m0."organization_id", m0."user_id" FROM "memberships" AS m0 []
iex(21)> Membership.read(%{email: "[email protected]"}, authorize?: false)
[debug] QUERY OK source="memberships" db=0.6ms queue=0.1ms idle=1341.0ms
SELECT m0."id", m0."role", m0."email", m0."invitation_token", m0."invited_at", m0."inserted_at", m0."updated_at", m0."organization_id", m0."user_id" FROM "memberships" AS m0 []
I would assume any filterable attributes would be automatically applied as a filter. You could also have an accepts option on the action to limit it certain filterable attributes.
13 replies
AEAsh Elixir
Created by Robert Graff on 7/28/2023 in #support
Key :editing_tenant not found
Just starting to explore AshAdmin.
** (KeyError) key :editing_tenant not found in: %{
__changed__: %{
clear_tenant: true,
flash: true,
id: true,
set_tenant: true,
tenant: true
},
__given__: %{
__changed__: %{
clear_tenant: true,
flash: true,
id: true,
set_tenant: true,
tenant: true
},
clear_tenant: "clear_tenant",
flash: %{},
id: "tenant_editor",
myself: %Phoenix.LiveComponent.CID{cid: 7},
set_tenant: "set_tenant",
socket: #Phoenix.LiveView.Socket<
id: "phx-F3YXwxGfYHWbADEG",
endpoint: KickplanWeb.Endpoint,
view: AshAdmin.PageLive,
parent_pid: nil,
root_pid: nil,
router: KickplanWeb.Router,
assigns: #Phoenix.LiveView.Socket.AssignsNotInSocket<>,
transport_pid: nil,
...
>,
tenant: nil
},
clear_tenant: "clear_tenant",
flash: %{},
id: "tenant_editor",
myself: %Phoenix.LiveComponent.CID{cid: 7},
set_tenant: "set_tenant",
socket: #Phoenix.LiveView.Socket<
id: "phx-F3YXwxGfYHWbADEG",
endpoint: KickplanWeb.Endpoint,
view: AshAdmin.PageLive,
parent_pid: nil,
root_pid: nil,
router: KickplanWeb.Router,
assigns: #Phoenix.LiveView.Socket.AssignsNotInSocket<>,
transport_pid: nil,
...
>,
tenant: nil
}
(ash_admin 0.9.0) lib/ash_admin/components/top_nav/tenant_form.ex:13: anonymous fn/2 in AshAdmin.Components.TopNav.TenantForm."render (overridable 1)"/1
(phoenix_live_view 0.19.5) lib/phoenix_live_view/diff.ex:386: Phoenix.LiveView.Diff.traverse/7
(phoenix_live_view 0.19.5) lib/phoenix_live_view/diff.ex:711: Phoenix.LiveView.Diff.render_component/9
(phoenix_live_view 0.19.5) lib/phoenix_live_view/diff.ex:657: anonymous fn/5 in Phoenix.LiveView.Diff.render_pending_components/6
(elixir 1.15.4) lib/enum.ex:2510: Enum."-reduce/3-lists^foldl/2-0-"/3
(stdlib 4.3) maps.erl:411: :maps.fold_1/3
(phoenix_live_view 0.19.5) lib/phoenix_live_view/diff.ex:629: Phoenix.LiveView.Diff.render_pending_components/6
(phoenix_live_view 0.19.5) lib/phoenix_live_view/diff.ex:143: Phoenix.LiveView.Diff.render/3
(phoenix_live_view 0.19.5) lib/phoenix_live_view/static.ex:252: Phoenix.LiveView.Static.to_rendered_content_tag/4
(phoenix_live_view 0.19.5) lib/phoenix_live_view/static.ex:135: Phoenix.LiveView.Static.render/3
(phoenix_live_view 0.19.5) lib/phoenix_live_view/controller.ex:39: Phoenix.LiveView.Controller.live_render/3
(phoenix 1.7.7) lib/phoenix/router.ex:430: Phoenix.Router.__call__/5
(kickplan 0.1.0) lib/kickplan_web/endpoint.ex:1: KickplanWeb.Endpoint.plug_builder_call/2
(kickplan 0.1.0) deps/plug/lib/plug/debugger.ex:136: KickplanWeb.Endpoint."call (overridable 3)"/2
(kickplan 0.1.0) lib/kickplan_web/endpoint.ex:1: KickplanWeb.Endpoint.call/2
(phoenix 1.7.7) lib/phoenix/endpoint/sync_code_reload_plug.ex:22: Phoenix.Endpoint.SyncCodeReloadPlug.do_call/4
(plug_cowboy 2.6.1) lib/plug/cowboy/handler.ex:11: Plug.Cowboy.Handler.init/2
(cowboy 2.10.0) /workspace/app/deps/cowboy/src/cowboy_handler.erl:37: :cowboy_handler.execute/2
(cowboy 2.10.0) /workspace/app/deps/cowboy/src/cowboy_stream_h.erl:306: :cowboy_stream_h.execute/3
(cowboy 2.10.0) /workspace/app/deps/cowboy/src/cowboy_stream_h.erl:295: :cowboy_stream_h.request_process/3
** (KeyError) key :editing_tenant not found in: %{
__changed__: %{
clear_tenant: true,
flash: true,
id: true,
set_tenant: true,
tenant: true
},
__given__: %{
__changed__: %{
clear_tenant: true,
flash: true,
id: true,
set_tenant: true,
tenant: true
},
clear_tenant: "clear_tenant",
flash: %{},
id: "tenant_editor",
myself: %Phoenix.LiveComponent.CID{cid: 7},
set_tenant: "set_tenant",
socket: #Phoenix.LiveView.Socket<
id: "phx-F3YXwxGfYHWbADEG",
endpoint: KickplanWeb.Endpoint,
view: AshAdmin.PageLive,
parent_pid: nil,
root_pid: nil,
router: KickplanWeb.Router,
assigns: #Phoenix.LiveView.Socket.AssignsNotInSocket<>,
transport_pid: nil,
...
>,
tenant: nil
},
clear_tenant: "clear_tenant",
flash: %{},
id: "tenant_editor",
myself: %Phoenix.LiveComponent.CID{cid: 7},
set_tenant: "set_tenant",
socket: #Phoenix.LiveView.Socket<
id: "phx-F3YXwxGfYHWbADEG",
endpoint: KickplanWeb.Endpoint,
view: AshAdmin.PageLive,
parent_pid: nil,
root_pid: nil,
router: KickplanWeb.Router,
assigns: #Phoenix.LiveView.Socket.AssignsNotInSocket<>,
transport_pid: nil,
...
>,
tenant: nil
}
(ash_admin 0.9.0) lib/ash_admin/components/top_nav/tenant_form.ex:13: anonymous fn/2 in AshAdmin.Components.TopNav.TenantForm."render (overridable 1)"/1
(phoenix_live_view 0.19.5) lib/phoenix_live_view/diff.ex:386: Phoenix.LiveView.Diff.traverse/7
(phoenix_live_view 0.19.5) lib/phoenix_live_view/diff.ex:711: Phoenix.LiveView.Diff.render_component/9
(phoenix_live_view 0.19.5) lib/phoenix_live_view/diff.ex:657: anonymous fn/5 in Phoenix.LiveView.Diff.render_pending_components/6
(elixir 1.15.4) lib/enum.ex:2510: Enum."-reduce/3-lists^foldl/2-0-"/3
(stdlib 4.3) maps.erl:411: :maps.fold_1/3
(phoenix_live_view 0.19.5) lib/phoenix_live_view/diff.ex:629: Phoenix.LiveView.Diff.render_pending_components/6
(phoenix_live_view 0.19.5) lib/phoenix_live_view/diff.ex:143: Phoenix.LiveView.Diff.render/3
(phoenix_live_view 0.19.5) lib/phoenix_live_view/static.ex:252: Phoenix.LiveView.Static.to_rendered_content_tag/4
(phoenix_live_view 0.19.5) lib/phoenix_live_view/static.ex:135: Phoenix.LiveView.Static.render/3
(phoenix_live_view 0.19.5) lib/phoenix_live_view/controller.ex:39: Phoenix.LiveView.Controller.live_render/3
(phoenix 1.7.7) lib/phoenix/router.ex:430: Phoenix.Router.__call__/5
(kickplan 0.1.0) lib/kickplan_web/endpoint.ex:1: KickplanWeb.Endpoint.plug_builder_call/2
(kickplan 0.1.0) deps/plug/lib/plug/debugger.ex:136: KickplanWeb.Endpoint."call (overridable 3)"/2
(kickplan 0.1.0) lib/kickplan_web/endpoint.ex:1: KickplanWeb.Endpoint.call/2
(phoenix 1.7.7) lib/phoenix/endpoint/sync_code_reload_plug.ex:22: Phoenix.Endpoint.SyncCodeReloadPlug.do_call/4
(plug_cowboy 2.6.1) lib/plug/cowboy/handler.ex:11: Plug.Cowboy.Handler.init/2
(cowboy 2.10.0) /workspace/app/deps/cowboy/src/cowboy_handler.erl:37: :cowboy_handler.execute/2
(cowboy 2.10.0) /workspace/app/deps/cowboy/src/cowboy_stream_h.erl:306: :cowboy_stream_h.execute/3
(cowboy 2.10.0) /workspace/app/deps/cowboy/src/cowboy_stream_h.erl:295: :cowboy_stream_h.request_process/3
I'm not sure how AshAdmin interacts with multitenancy. For context, some of my apis are multitenancy but not all of them. The tenant for web is via a url slug. For graphql, it's a header. When admin starts, I'm assuming there won't be a tenant.
9 replies
AEAsh Elixir
Created by Robert Graff on 7/20/2023 in #support
How do relationships with composite keys work?
I found this mention of composite keys in the docs:
has_many :composite_key_posts, MyApp.CompositeKeyPost do
destination_attribute :author_id
end
has_many :composite_key_posts, MyApp.CompositeKeyPost do
destination_attribute :author_id
end
Ideally I need something like:
belongs_to :widget, MyApp.Widget do
source_attribute: [:scope_key, :source_key]
destination_attribute: [:scope_key, :destination_key]
end
belongs_to :widget, MyApp.Widget do
source_attribute: [:scope_key, :source_key]
destination_attribute: [:scope_key, :destination_key]
end
A similar effect to have scope_key being a tenant.
10 replies
AEAsh Elixir
Created by Robert Graff on 7/14/2023 in #support
Setting defaults for a form input that is an argument and not an attribute?
I have an update action that takes an argument. In the form for this action, I need to set the default value for the form input. The default value is dependent on the current state of the resource being updated. I explored using the default option for an action argument; however, It accepts a zero-argument function, so there's no way to get the resource being updated. I explored using the prepare_params option for AshPhoenix.Form.for_update but I couldn't find any examples. It's arrity 2 and receives the params but is only called when the form is validated or submitted. Side quest(ion): what's the difference between prepare_params and transform_params if they're both called only on validate and submit?
17 replies
AEAsh Elixir
Created by Robert Graff on 7/13/2023 in #support
Understanding value_is_key option when managing relationships
Given a relation that uses a ci_string (not uuid):
belongs_to :feature, Environments.Feature do
allow_nil? false
source_attribute :feature_key
attribute_type :ci_string
destination_attribute :key
end
belongs_to :feature, Environments.Feature do
allow_nil? false
source_attribute :feature_key
attribute_type :ci_string
destination_attribute :key
end
I would expect this change to work using value_is_key option:
argument :feature_key, :ci_string, allow_nil?: false
change manage_relationship(:feature_key, :feature,
type: :append,
value_is_key: :key,
use_identities: [:key]
)
argument :feature_key, :ci_string, allow_nil?: false
change manage_relationship(:feature_key, :feature,
type: :append,
value_is_key: :key,
use_identities: [:key]
)
But it gives me this error, but I thought the purpose of value_is_key is to not provide a struct:
** (Ash.Error.Invalid) Input Invalid
* Invalid value provided for feature: cannot provide structs that don't match the destination.
** (Ash.Error.Invalid) Input Invalid
* Invalid value provided for feature: cannot provide structs that don't match the destination.
For reference, this more verbose change does work:
argument :feature_key, :ci_string, allow_nil?: false

change fn changeset, _ ->
case Ash.Changeset.get_argument(changeset, :feature_key) do
nil ->
changeset

feature_key ->
Ash.Changeset.manage_relationship(
changeset,
:feature,
%{key: feature_key},
type: :append,
use_identities: [:key]
)
end
end
argument :feature_key, :ci_string, allow_nil?: false

change fn changeset, _ ->
case Ash.Changeset.get_argument(changeset, :feature_key) do
nil ->
changeset

feature_key ->
Ash.Changeset.manage_relationship(
changeset,
:feature,
%{key: feature_key},
type: :append,
use_identities: [:key]
)
end
end
20 replies
AEAsh Elixir
Created by Robert Graff on 7/5/2023 in #support
How to set belongs_to on in a form
I have a create action for Plan that sets the Product (Plan belongs_to Product)
create :create do
primary? true
argument :product, :map, allow_nil?: false
change manage_relationship(:product, :product, type: :append)
end
create :create do
primary? true
argument :product, :map, allow_nil?: false
change manage_relationship(:product, :product, type: :append)
end
I have a form that I create like this...
create_plan_form =
AshPhoenix.Form.for_create(Plan, :create, api: Environments)
|> to_form()
|> AshPhoenix.Form.add_form([:product])
create_plan_form =
AshPhoenix.Form.for_create(Plan, :create, api: Environments)
|> to_form()
|> AshPhoenix.Form.add_form([:product])
and then a select input
<.inputs_for :let={product_form} field={@create_plan_form[:product]}>
<.input
type="select"
field={{product_form, :id}}
options={@product_options}
label="Product"
/>
</.inputs_for>
<.inputs_for :let={product_form} field={@create_plan_form[:product]}>
<.input
type="select"
field={{product_form, :id}}
options={@product_options}
label="Product"
/>
</.inputs_for>
This raises an error:
product at path [] must be configured in the form to be used with `inputs_for`.
product at path [] must be configured in the form to be used with `inputs_for`.
I tried forms: [auto?: true] and then I don't get an error but I also don't get any field.
3 replies
AEAsh Elixir
Created by Robert Graff on 6/26/2023 in #support
AshAuthentication identity :token_context
With the latest release of AshAuthention, I'm getting a DSL error.
** (EXIT from #PID<0.96.0>) an exception was raised:
** (Spark.Error.DslError) [MyApp.Users.UserToken]
identities -> token_context:
All identity keys must be attributes. Got: :context
** (EXIT from #PID<0.96.0>) an exception was raised:
** (Spark.Error.DslError) [MyApp.Users.UserToken]
identities -> token_context:
All identity keys must be attributes. Got: :context
identities do
identity :token_context, [:context, :token]
end
identities do
identity :token_context, [:context, :token]
end
My git blame says I added this when migrating to Ash, but I don't remember why or see it referenced in any of the guides. Any idea what I was trying to do here?
7 replies
AEAsh Elixir
Created by Robert Graff on 6/15/2023 in #support
Any examples of using can?
I'm trying to see if a user (actor) can perform an update action on target user.
iex(9)> MyApp.Users.can?({target_user, :update}, actor)
** (ArgumentError) Invalid action/query/changeset "nil"
(ash 2.9.19) lib/ash/api/api.ex:610: Ash.Api.can/4
(ash 2.9.19) lib/ash/api/api.ex:522: Ash.Api.can?/4
iex:9: (file)
iex(9)> MyApp.Users.can?({target_user, :update}, actor)
** (ArgumentError) Invalid action/query/changeset "nil"
(ash 2.9.19) lib/ash/api/api.ex:610: Ash.Api.can/4
(ash 2.9.19) lib/ash/api/api.ex:522: Ash.Api.can?/4
iex:9: (file)
10 replies
AEAsh Elixir
Created by Robert Graff on 6/8/2023 in #support
Bulk create form
I currently have a phoenix form to create an invite. The form is connected to the create action on the Invite resource. The only field is email. I want to change the form to accept multiple email addresses--comma separated--and then create multiple invites. The create action currently generates a token and sends an email, so it's bit more complicated than a db insert. I haven't used manual actions, the new bulk_create feature, or Ash.Flow yet. I'm not sure which is the best route. I'm thinking of adding a manual bulk_create action to my resource that calls the existing create for each email address. Feedback?
5 replies
AEAsh Elixir
Created by Robert Graff on 5/5/2023 in #support
Custom reset password flow not passing policy checks
I have a custom live view with a reset password form.
AshPhoenix.Form.for_read(User, :request_password_reset_with_password, api: Iterup.Users)
|> to_form()
AshPhoenix.Form.for_read(User, :request_password_reset_with_password, api: Iterup.Users)
|> to_form()
That I submit like this
AshPhoenix.Form.submit(socket.assigns.reset_password_form, params: params)
AshPhoenix.Form.submit(socket.assigns.reset_password_form, params: params)
I would expect this to pass policy checks, but it doesn't
Policy Breakdown
Policy | ⛔:
condition: action.type == :read
authorize if: AshAuthentication is performing this interaction || 🔎
forbid unless: actor is present ||
authorize if: id == {:_actor, :id} | ? |
authorize if: record.memberships.account.memberships.user == actor | ? |
Policy Breakdown
Policy | ⛔:
condition: action.type == :read
authorize if: AshAuthentication is performing this interaction || 🔎
forbid unless: actor is present ||
authorize if: id == {:_actor, :id} | ? |
authorize if: record.memberships.account.memberships.user == actor | ? |
(I moved the check out of a bypass to have it listed in Policy Breakdown explicitly showing it as a fail)
4 replies
AEAsh Elixir
Created by Robert Graff on 5/2/2023 in #support
Best way to skip tenant check on a query
In my app I have users and accounts. The accounts are tenants. Users have and belong to many accounts through memberships. I've made the memberships resource multi-tenant, but now the users can't load their memberships. Memberships are really co-owned. Should I remove the multi-tenancy? Or is there a way for a query to ignore the tenant check?
14 replies
AEAsh Elixir
Created by Robert Graff on 4/30/2023 in #support
Create action with multiple nested resources
This is the action to register an account. There are 3 resources at play. An account has many memberships and every membership belongs to a user. The membership resource is simply as has_and_belongs_to_many with a role on it. When a new account is created, it should always create the account and membership for the owner/user. The user resource has an identity on email and is only created if the user doesn't already exist. What happens is the membership fails to insert because the user_id is nil.
actions do
create :register do
accept([:name, :slug])
allow_nil_input([:slug])

argument(:email, :string, allow_nil?: false)
...

change(fn changeset, _context ->
email = changeset |> Ash.Changeset.get_argument(:email)

changeset
|> Ash.Changeset.manage_relationship(
:memberships,
[%{role: "owner", user: %{email: email}}],
type: :create
)
end)
end
end
actions do
create :register do
accept([:name, :slug])
allow_nil_input([:slug])

argument(:email, :string, allow_nil?: false)
...

change(fn changeset, _context ->
email = changeset |> Ash.Changeset.get_argument(:email)

changeset
|> Ash.Changeset.manage_relationship(
:memberships,
[%{role: "owner", user: %{email: email}}],
type: :create
)
end)
end
end
15 replies
AEAsh Elixir
Created by Robert Graff on 4/27/2023 in #support
Set error message on attribute using AshAuthentication.Strategy.Password.PasswordValidation
When a password validation fails on a liveview form submitted. How do I get an error on the :current_password attribute?
AshPhoenix.Form.submit(socket.assigns.password_form, params: params, actor: socket.assigns.current_user)
...

validate {AshAuthentication.Strategy.Password.PasswordValidation, password_argument: :current_password} do
only_when_valid?(true)
before_action?(true)
message("Password incorrect")
end
AshPhoenix.Form.submit(socket.assigns.password_form, params: params, actor: socket.assigns.current_user)
...

validate {AshAuthentication.Strategy.Password.PasswordValidation, password_argument: :current_password} do
only_when_valid?(true)
before_action?(true)
message("Password incorrect")
end
41 replies
AEAsh Elixir
Created by Robert Graff on 4/27/2023 in #support
Testing with log_in_user
I'm writing tests and have a helper to log_in_user that I found in ash-hq source.
def log_in_user(conn, user) do
conn
|> Phoenix.ConnTest.init_test_session(%{})
|> Plug.Conn.put_session(:user_token, user.__metadata__.token)
end
def log_in_user(conn, user) do
conn
|> Phoenix.ConnTest.init_test_session(%{})
|> Plug.Conn.put_session(:user_token, user.__metadata__.token)
end
But user.__metadata__ == %{} which makes sense since I'm generating the user with Ash.Seed. In ash-hq the user is created by a register action which I assume sets the metadata, but don't see how. I don't have an equivalent register action. How do I generate this token?
3 replies
AEAsh Elixir
Created by Robert Graff on 4/25/2023 in #support
Resolve notifications warning in when running tests
I'm getting this warning when running my test suite. It's coming from (I think) AshAuthentication sending confirmation emails. I'm not sure how to resolve it.
[warning] Missed 1 notifications in action Iterup.Accounts.UserToken.store_confirmation_changes.

This happens when the resources are in a transaction, and you did not pass
`return_notifications?: true`. If you are in a changeset hook, you can
return the notifications. If not, you can send the notifications using
`Ash.Notifier.notify/1` once your resources are out of a transaction.
[warning] Missed 1 notifications in action Iterup.Accounts.UserToken.store_confirmation_changes.

This happens when the resources are in a transaction, and you did not pass
`return_notifications?: true`. If you are in a changeset hook, you can
return the notifications. If not, you can send the notifications using
`Ash.Notifier.notify/1` once your resources are out of a transaction.
3 replies
AEAsh Elixir
Created by Robert Graff on 4/21/2023 in #support
How to compare a seed and loaded resource?
I thought strip_metadata() would make the comparable
user = Ash.Seed.seed!(%User{...})
loaded_user = User.get_by_id!(user.id) |> Ash.Test.strip_metadata()
assert loaded_user == user

Assertion with == failed
code: assert loaded_user == user
left: #Iterup.Accounts.User<account: #Ash.NotLoaded<:relationship>, __meta__: #Ecto.Schema.Metadata<>, id: "d4a9ed2e-cfae-4b18-b2a2-d07a0d426854", email: #Ash.CiString<"[email protected]">, name: "Mohammad Abshire", avatar_path: nil, confirmed_at: nil, inserted_at: ~U[2023-04-21 15:49:04.534852Z], updated_at: ~U[2023-04-21 15:49:04.534852Z], account_id: "eeb36be1-a423-4569-81ac-c1256de4fda3", aggregates: %{}, calculations: %{}, __order__: nil, ...>
right: #Iterup.Accounts.User<account: #Iterup.Accounts.Account<users: #Ash.NotLoaded<:relationship>, invites: #Ash.NotLoaded<:relationship>, __meta__: #Ecto.Schema.Metadata<:loaded, "accounts">, id: "eeb36be1-a423-4569-81ac-c1256de4fda3", name: "Leffler-Connelly", slug: "leffler-connelly", inserted_at: ~U[2023-04-21 15:49:04.375199Z], updated_at: ~U[2023-04-21 15:49:04.375199Z], aggregates: %{}, calculations: %{}, __order__: nil, ...>, __meta__: #Ecto.Schema.Metadata<:loaded, "users">, id: "d4a9ed2e-cfae-4b18-b2a2-d07a0d426854", email: "[email protected]", name: "Mohammad Abshire", avatar_path: nil, confirmed_at: nil, inserted_at: ~U[2023-04-21 15:49:04.534852Z], updated_at: ~U[2023-04-21 15:49:04.534852Z], account_id: "eeb36be1-a423-4569-81ac-c1256de4fda3", aggregates: %{}, calculations: %{}, __order__: nil, ...>
user = Ash.Seed.seed!(%User{...})
loaded_user = User.get_by_id!(user.id) |> Ash.Test.strip_metadata()
assert loaded_user == user

Assertion with == failed
code: assert loaded_user == user
left: #Iterup.Accounts.User<account: #Ash.NotLoaded<:relationship>, __meta__: #Ecto.Schema.Metadata<>, id: "d4a9ed2e-cfae-4b18-b2a2-d07a0d426854", email: #Ash.CiString<"[email protected]">, name: "Mohammad Abshire", avatar_path: nil, confirmed_at: nil, inserted_at: ~U[2023-04-21 15:49:04.534852Z], updated_at: ~U[2023-04-21 15:49:04.534852Z], account_id: "eeb36be1-a423-4569-81ac-c1256de4fda3", aggregates: %{}, calculations: %{}, __order__: nil, ...>
right: #Iterup.Accounts.User<account: #Iterup.Accounts.Account<users: #Ash.NotLoaded<:relationship>, invites: #Ash.NotLoaded<:relationship>, __meta__: #Ecto.Schema.Metadata<:loaded, "accounts">, id: "eeb36be1-a423-4569-81ac-c1256de4fda3", name: "Leffler-Connelly", slug: "leffler-connelly", inserted_at: ~U[2023-04-21 15:49:04.375199Z], updated_at: ~U[2023-04-21 15:49:04.375199Z], aggregates: %{}, calculations: %{}, __order__: nil, ...>, __meta__: #Ecto.Schema.Metadata<:loaded, "users">, id: "d4a9ed2e-cfae-4b18-b2a2-d07a0d426854", email: "[email protected]", name: "Mohammad Abshire", avatar_path: nil, confirmed_at: nil, inserted_at: ~U[2023-04-21 15:49:04.534852Z], updated_at: ~U[2023-04-21 15:49:04.534852Z], account_id: "eeb36be1-a423-4569-81ac-c1256de4fda3", aggregates: %{}, calculations: %{}, __order__: nil, ...>
Are there any showcase apps that have some good example test suites?
8 replies
AEAsh Elixir
Created by Robert Graff on 4/18/2023 in #support
Identity on two attributes causing errors
I have an identity on two attributes (context, and token). I would expect this to require a unique context <> token. But it's not allowing duplicate contexts. Here's my resource:
defmodule Iterup.Account.UserToken do
use Ash.Resource,
data_layer: AshPostgres.DataLayer

@rand_size 32

postgres do
table("user_tokens")
repo(Iterup.Repo)
end

attributes do
uuid_primary_key(:id)

attribute :sent_to, :ci_string, allow_nil?: false
attribute :token, :binary, allow_nil?: false
attribute :context, :string, allow_nil?: false
attribute :confirmed_at, :utc_datetime

create_timestamp :inserted_at
end

relationships do
belongs_to :user, Iterup.Account.User
end

identities do
identity :context_token, [:context, :token]
end

code_interface do
define_for(Iterup.Account)
define(:create_session_token, action: :session_token)
end

actions do
create :session_token do
accept [:sent_to]
argument :user_id, :uuid, allow_nil?: false
change manage_relationship(:user_id, :user, type: :append)
change set_attribute(:token, :crypto.strong_rand_bytes(@rand_size))
change set_attribute(:context, "session")
end
end
end
defmodule Iterup.Account.UserToken do
use Ash.Resource,
data_layer: AshPostgres.DataLayer

@rand_size 32

postgres do
table("user_tokens")
repo(Iterup.Repo)
end

attributes do
uuid_primary_key(:id)

attribute :sent_to, :ci_string, allow_nil?: false
attribute :token, :binary, allow_nil?: false
attribute :context, :string, allow_nil?: false
attribute :confirmed_at, :utc_datetime

create_timestamp :inserted_at
end

relationships do
belongs_to :user, Iterup.Account.User
end

identities do
identity :context_token, [:context, :token]
end

code_interface do
define_for(Iterup.Account)
define(:create_session_token, action: :session_token)
end

actions do
create :session_token do
accept [:sent_to]
argument :user_id, :uuid, allow_nil?: false
change manage_relationship(:user_id, :user, type: :append)
change set_attribute(:token, :crypto.strong_rand_bytes(@rand_size))
change set_attribute(:context, "session")
end
end
end
Here's the error:
# First call is successful
session_token_1 = UserToken.create_session_token!(%{user_id: user.id, sent_to: user.email})

...

# Second call is unsuccessful
session_token_2 = UserToken.create_session_token!(%{user_id: user.id, sent_to: user.email})
...
↳ anonymous fn/3 in Ash.Changeset.with_hooks/3, at: lib/ash/changeset/changeset.ex:1573
** (Ash.Error.Invalid) Input Invalid

* Invalid value provided for context: has already been taken.
# First call is successful
session_token_1 = UserToken.create_session_token!(%{user_id: user.id, sent_to: user.email})

...

# Second call is unsuccessful
session_token_2 = UserToken.create_session_token!(%{user_id: user.id, sent_to: user.email})
...
↳ anonymous fn/3 in Ash.Changeset.with_hooks/3, at: lib/ash/changeset/changeset.ex:1573
** (Ash.Error.Invalid) Input Invalid

* Invalid value provided for context: has already been taken.
What am I missing?
9 replies
AEAsh Elixir
Created by Robert Graff on 4/17/2023 in #support
Handling an %Ash.Error.Invalid{} in LiveView
Sorry for the noob question, but the AshPhoenix guide doesn't show any error handling. This is the code that I assumed would work, but update returns a {:error, %Ash.Error.Invalid{} } How do I get those errors onto the form?
def handle_event("update-slug", params, socket) do
current_account = socket.assigns.current_account

case Account.update(current_account, params, actor: socket.assigns.current_user) do
{:ok, applied_account} ->
info = "Slug changed successfully."

socket =
socket
|> put_flash(:info, info)
|> assign(:current_account, applied_account)
|> push_navigate(to: ~p"/#{applied_account.slug}/settings")

{:noreply, socket}

{:error, slug_form} ->
{:noreply, assign(socket, :slug_form, slug_form)}
end
end
def handle_event("update-slug", params, socket) do
current_account = socket.assigns.current_account

case Account.update(current_account, params, actor: socket.assigns.current_user) do
{:ok, applied_account} ->
info = "Slug changed successfully."

socket =
socket
|> put_flash(:info, info)
|> assign(:current_account, applied_account)
|> push_navigate(to: ~p"/#{applied_account.slug}/settings")

{:noreply, socket}

{:error, slug_form} ->
{:noreply, assign(socket, :slug_form, slug_form)}
end
end
15 replies