AE
Ash Elixir•3y ago
Blibs

GraphQL query of resource stops working after upsert of it

This seems a little bizarre.. I have this in my seeds.exs:
super_user_args = %{
first_name: "Super",
surname: "Admin",
phone_number: "(800) 345-2747",
password: super_user_password,
password_confirmation: super_user_password
}

super_admin =
Accounts.User
|> Ash.Changeset.new()
|> Ash.Changeset.for_create(:register_with_password, super_user_args,
upsert?: true,
upsert_identity: :unique_email
)
|> Ash.Changeset.force_change_attribute(:confirmed_at, DateTime.utc_now())
|> Ash.Changeset.force_change_attribute(:roles, [:super_admin])
|> Accounts.create!()
super_user_args = %{
first_name: "Super",
surname: "Admin",
phone_number: "(800) 345-2747",
password: super_user_password,
password_confirmation: super_user_password
}

super_admin =
Accounts.User
|> Ash.Changeset.new()
|> Ash.Changeset.for_create(:register_with_password, super_user_args,
upsert?: true,
upsert_identity: :unique_email
)
|> Ash.Changeset.force_change_attribute(:confirmed_at, DateTime.utc_now())
|> Ash.Changeset.force_change_attribute(:roles, [:super_admin])
|> Accounts.create!()
First time that this runs in an empty database, it will create the row. After this, I can query that users with GraphQL without any problem. But, if afterwards I run my seeds.exs file again, it will run the above code again and update that row in the database. After that, if I try to query from GraphQL with first_name, surname or phone_number which are stored as encrypted data, I get:
[error] #PID<0.1159.0> running Phoenix.Endpoint.SyncCodeReloadPlug (connection #PID<0.931.0>, stream id 15) terminated
Server: localhost:4000 (http)
Request: POST /playground
** (exit) an exception was raised:
** (Jason.EncodeError) invalid byte 0xB5 in <<1, 10, 65, 69, 83, 46, 71, 67, 77, 46, 86, 49, 181, 37, 78, 61, 19, 46, 238, 148, 121, 105, 87, 186, 181, 196, 38, 13, 156, 218, 53, 100, 21, 178, 154, 233, 81, 224, 33, 199, 67, 98, 170, 225, 228, 85, 131, 227, 1, 186, ...>>
(jason 1.4.0) lib/jason.ex:164: Jason.encode!/2
(absinthe_plug 1.5.8) lib/absinthe/plug.ex:598: Absinthe.Plug.encode/4
(phoenix 1.7.0-rc.2) lib/phoenix/router/route.ex:42: Phoenix.Router.Route.call/2
[error] #PID<0.1159.0> running Phoenix.Endpoint.SyncCodeReloadPlug (connection #PID<0.931.0>, stream id 15) terminated
Server: localhost:4000 (http)
Request: POST /playground
** (exit) an exception was raised:
** (Jason.EncodeError) invalid byte 0xB5 in <<1, 10, 65, 69, 83, 46, 71, 67, 77, 46, 86, 49, 181, 37, 78, 61, 19, 46, 238, 148, 121, 105, 87, 186, 181, 196, 38, 13, 156, 218, 53, 100, 21, 178, 154, 233, 81, 224, 33, 199, 67, 98, 170, 225, 228, 85, 131, 227, 1, 186, ...>>
(jason 1.4.0) lib/jason.ex:164: Jason.encode!/2
(absinthe_plug 1.5.8) lib/absinthe/plug.ex:598: Absinthe.Plug.encode/4
(phoenix 1.7.0-rc.2) lib/phoenix/router/route.ex:42: Phoenix.Router.Route.call/2
I don't get why, but when the update runs, somehow it generates an invalid binary for these specific fields
14 Replies
ZachDaniel
ZachDaniel•3y ago
what on earth Oh, 🤔 I think I might see the issue well, not 100% sure When you do a simple create, what does the first_name value look like? and how are you doing the encryption? I have a feeling this is in some way related to how your encrypted type is implemented
Blibs
BlibsOP•3y ago
I basically copied whay you did in AshHq hahaha
attributes
...
attribute :encrypted_first_name, Marketplace.Types.EncryptedString do
allow_nil? false
private? true
end
...
end

calculations do
calculate :first_name,
:string,
{Marketplace.Calculations.Decrypt, field: :encrypted_first_name}
end
attributes
...
attribute :encrypted_first_name, Marketplace.Types.EncryptedString do
allow_nil? false
private? true
end
...
end

calculations do
calculate :first_name,
:string,
{Marketplace.Calculations.Decrypt, field: :encrypted_first_name}
end
The type:
defmodule Marketplace.Types.EncryptedString do
@moduledoc "Represents a string that is encrypted when cast as input"

use Ash.Type

@impl true
def storage_type, do: :binary

@impl true
def cast_input(nil, _), do: {:ok, nil}

def cast_input(value, _) do
Marketplace.Vault.encrypt(value)
end

@impl true
def cast_stored(nil, _), do: {:ok, nil}

def cast_stored(value, _) when is_binary(value) do
{:ok, value}
end

def cast_stored(_, _), do: :error

@impl true
def dump_to_native(nil, _), do: {:ok, nil}
def dump_to_native(value, _) when is_binary(value), do: {:ok, value}
def dump_to_native(_, _), do: :error
end
defmodule Marketplace.Types.EncryptedString do
@moduledoc "Represents a string that is encrypted when cast as input"

use Ash.Type

@impl true
def storage_type, do: :binary

@impl true
def cast_input(nil, _), do: {:ok, nil}

def cast_input(value, _) do
Marketplace.Vault.encrypt(value)
end

@impl true
def cast_stored(nil, _), do: {:ok, nil}

def cast_stored(value, _) when is_binary(value) do
{:ok, value}
end

def cast_stored(_, _), do: :error

@impl true
def dump_to_native(nil, _), do: {:ok, nil}
def dump_to_native(value, _) when is_binary(value), do: {:ok, value}
def dump_to_native(_, _), do: :error
end
The calculation:
defmodule Marketplace.Calculations.Decrypt do
@moduledoc "Decrypts a given value on demand"

use Ash.Calculation

def calculate(records, opts, _) do
{:ok,
Enum.map(records, fn record ->
record
|> Map.get(opts[:field])
|> case do
nil ->
nil

value ->
Marketplace.Vault.decrypt!(value)
end
end)}
end

def select(_, opts, _) do
[opts[:field]]
end

def load(_, opts, _) do
[opts[:field]]
end
end
defmodule Marketplace.Calculations.Decrypt do
@moduledoc "Decrypts a given value on demand"

use Ash.Calculation

def calculate(records, opts, _) do
{:ok,
Enum.map(records, fn record ->
record
|> Map.get(opts[:field])
|> case do
nil ->
nil

value ->
Marketplace.Vault.decrypt!(value)
end
end)}
end

def select(_, opts, _) do
[opts[:field]]
end

def load(_, opts, _) do
[opts[:field]]
end
end
ZachDaniel
ZachDaniel•3y ago
interesting. Okay, lemme see what happens when I upsert ash_hq stuff 😆
Blibs
BlibsOP•3y ago
I think this will help
No description
Blibs
BlibsOP•3y ago
the user is the user when it was created, the user2 is the user after the upsert As you can see, the user will load the first_name correctly, the user after the upsert will put binary data into the first_name field for some reason
ZachDaniel
ZachDaniel•3y ago
🤔 yeah, this may actually be a problem with the way I implemented that type... The basic issue is that we can't do a repeatable cast of that value Alright, so we've nailed it down 🙂 Someone else had a problem with the EncryptedString type, and basically the answer is not to use types to encrypt values. I'll push up an update to ash_hq showing how I'm doing it now Alright
ZachDaniel
ZachDaniel•3y ago
The main thing you'll want to notice is the migration, you have to do some goofy things if you've already deployed your app but if its local dev then you can just blow it all away and regenerate migrations 😄
Blibs
BlibsOP•3y ago
good thing that it's only local dev for me right now hahahah Btw, shouldn't the attributes be :binary instead of :string in this case?
Blibs
BlibsOP•3y ago
Here to be more specific:
No description
ZachDaniel
ZachDaniel•3y ago
I'm base64 encoding/decoding now for compatibility of the pattern because someone else had issues using encoded :binary in embedded attributes
Blibs
BlibsOP•3y ago
Ah, I see
ZachDaniel
ZachDaniel•3y ago
(because they are json in the database and can't have binary) So I figured the ash_hq example should do that but you can make them :binary and remove the base64 encodign/decoding
Blibs
BlibsOP•3y ago
Thanks Zach, I will try that out tomorrow morning!

Did you find this page helpful?