I’m creating an upsert action, but it returns an error when I try to use it:
Member2
|> Ash.Changeset.for_create(:signup_with_auth, %{email: email}, tenant: group.id)
|> Ash.create()
{
:error,
%Ash.Error.Unknown{changeset: "#Changeset<>",
errors: [%Ash.Error.Unknown.UnknownError{
error: "** (Postgrex.Error) ERROR 42P10 (invalid_column_reference) there is no unique or exclusion constraint matching the ON CONFLICT specification",
field: nil, value: nil, splode: Ash.Error, bread_crumbs: [], vars: [], path: [], stacktrace: #Splode.Stacktrace<>, class: :unknown}]}
}
Here are the important parts of the resource module:
defmodule Member2 do
use Ash.Resource,
domain: GF.Domain,
data_layer: AshPostgres.DataLayer,
authorizers: [Ash.Policy.Authorizer]
attributes do
attribute :email, :string, public?: true
attribute :group_id, :integer, public?: true
end
actions do
defaults [:read]
create :signup_with_auth do
upsert? true
upsert_identity :unique_email
accept [:name, :email]
end
end
identities do
identity :unique_email, [:email]
# Also tried this:
# identity :unique_email, [:email, :group_id]
end
multitenancy do
strategy :attribute
attribute :group_id
global? true
end
end
There is a similar upsert action elsewhere in my application that works fine, but that resource isn’t multitenant, so I wonder if the problem has something to do with multitenancy. The other thing I considered is the the foreign key constraint that is currently defined in the database. Are there requirements I need to follow for the FK constraint?
attribute multi tenancy could play a part, but IIRC we’ve accounted for that when upsetting. Have you run mix ash.codegen? Are there no pending migrations?
You’ll get a compile error telling you to add a calculations to SQL option to express that calculation in raw SQL, then it can generate an appropriate index, and can use the index in upserts etc.
That pretty much got me there. I just had to add this
postgres do
calculations_to_sql lower_email: "LOWER(email)"
end
Also, I removed the tenant attribute from the identities, like this:
identities do
identity :members_unique_email, [:lower_email]
end
I had to make some other changes unrelated to this particular issue, but the action was eventually successful. Also, after running ash.codegen and deleting the generated migration file, then running ash.codegen a second time produced no further migration file.
Sure, so I want a user to be able to register themselves with an email address and name. That’s the upsert action described in the beginning. But when I run it from AshGraphql, it fails with the above-mentioned error. According to the field policies of this resource, the actor should have access to the email field as long as the record is the actor’s own record. There’s aleady a self-update action that works this way, and it works in AshGraphql without issues. The email attribute is read without issue. But this particular action is an upsert, and it’s giving the error described above.
Here’s the self-update mutation that works:
actions do
update :update_self do
accept [:name]
require_atomic? false
change(&GF.Members.MemberActions.update_self/2)
change(&GF.Members.MemberActions.create_snapshot/2)
end
read :read_self do
get? true
manual GF.Members.CurrentActorRead
end
end
policies do
bypass action [:read_self, :update_self] do
authorize_if expr(^actor(:__struct__) == ^__MODULE__ and id == ^actor(:id))
end
end
update :update_self_member2, :update_self do
identity false
read_action :read_self
end
defmodule GF.Members.ActiveMemberPolicy do
use Ash.Policy.SimpleCheck
# This is used when logging a breakdown of how a policy is applied - see Logging below.
def describe(_) do
"Member is active and has given role"
end
def match?(%_{} = member, %{resource: _resource} = _context, opts) do
active? =
case member do
%{status: :active} -> true
_other -> false
end
cond do
opts[:role] ->
:"Elixir.GF.Members.Member".can_take_role_action?(member, opts[:role])
true ->
active?
end
end
def match?(_actor, _context, _opts) do
false
end
end
Hmm…okay, could you try to work up a reproduction/issue in that case? I will have to investigate. Perhaps when loading data for an upsert we’re not properly providing the actor? Hard to say.
I was incorrect. In the AshGraphl test, the record did actually get created, but:
There is an "errors" object at the root of the response body, that describes a "forbidden_field" of "email".
The return "email" is nil, which I suppose is expected if the user didn’t have the ability to read the email. But according to the field policies, the user should have that ability.
This reproduction uses the latest versions of ash, ash_postgres, and ash_graphql, and these newer version show more information about the policy failure (see comments in the code).
It appears the policy doesn’t like the fact that there’s no actor. I see the problem, but I’m not sure what that the solution might be.
UPDATE: Eliminated multitenancy as a contributing factor. The example above has been updated.
UPDATE 2: Eliminated unique index name as a contributing factor.
UPDATE 3: Eliminate lower() index constraint as a contributing factor.
I think what you’ll want to do, is make exceptions in your policies for this specific action. i.e you can see the email of a user you just signed up.
I’m not sure how to go about doing that. Do you mean, make an exception within the field policies?
Like this?
field_policies do
field_policy [:email, :name] do
authorize_if action(:signup)
end
end
The test still fails on the field policy, because internally the :read action is being called. It’s the :read action that’s causing the error. How do I allow reading fields for the :read action for this case without exposing fields to everyone else?
Ah, okay, right. That is a problem. This is a pretty special case, but obviously needs a solution. I’m considering something like authorize_if just_created_by(:signup), and we can set context on the changeset to support that after creating a record.