Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions priv/resource_snapshots/test_repo/rsvps/20251002180954.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"attributes": [
{
"allow_nil?": false,
"default": "fragment(\"gen_random_uuid()\")",
"generated?": false,
"precision": null,
"primary_key?": true,
"references": null,
"scale": null,
"size": null,
"source": "id",
"type": "uuid"
},
{
"allow_nil?": false,
"default": "0",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "response",
"type": "integer"
}
],
"base_filter": null,
"check_constraints": [],
"custom_indexes": [],
"custom_statements": [],
"has_create_action": true,
"hash": "8ADC3A631361A18B1B9A2070D5E8477428EDE39DE3B43FA5FF8E50CE7710B9E5",
"identities": [],
"multitenancy": {
"attribute": null,
"global": null,
"strategy": null
},
"repo": "Elixir.AshPostgres.TestRepo",
"schema": null,
"table": "rsvps"
}
20 changes: 20 additions & 0 deletions priv/test_repo/migrations/20251002180954_migrate_resources62.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
defmodule AshPostgres.TestRepo.Migrations.MigrateResources62 do
@moduledoc """
Updates resources based on their most recent snapshots.
This file was autogenerated with `mix ash_postgres.generate_migrations`
"""

use Ecto.Migration

def up do
create table(:rsvps, primary_key: false) do
add(:id, :uuid, null: false, default: fragment("gen_random_uuid()"), primary_key: true)
add(:response, :integer, null: false, default: 0)
end
end

def down do
drop(table(:rsvps))
end
end
1 change: 1 addition & 0 deletions test/support/domain.ex
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ defmodule AshPostgres.Test.Domain do
resource(AshPostgres.Test.Order)
resource(AshPostgres.Test.Chat)
resource(AshPostgres.Test.Message)
resource(AshPostgres.Test.RSVP)
end

authorization do
Expand Down
42 changes: 42 additions & 0 deletions test/support/resources/rsvp.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
defmodule AshPostgres.Test.RSVP do
@moduledoc false
use Ash.Resource,
domain: AshPostgres.Test.Domain,
data_layer: AshPostgres.DataLayer

postgres do
table "rsvps"
repo AshPostgres.TestRepo
end

actions do
default_accept(:*)
defaults([:create, :read, :update, :destroy])

# Uses an expression with an array of atoms for a custom type backed by integers.
update :clear_response do
change(
atomic_update(
:response,
expr(
if response in [:accepted, :declined] do
:awaiting
else
response
end
)
)
)
end
end

attributes do
uuid_primary_key(:id)

attribute(:response, AshPostgres.Test.Types.Response,
allow_nil?: false,
public?: true,
default: 0
)
end
end
75 changes: 75 additions & 0 deletions test/support/types/response.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
defmodule AshPostgres.Test.Types.Response do
@moduledoc false
use Ash.Type
use AshPostgres.Type
require Ash.Expr

@atoms_to_ints %{accepted: 1, declined: 2, awaiting: 0}
@ints_to_atoms Map.new(@atoms_to_ints, fn {k, v} -> {v, k} end)
@atom_values Map.keys(@atoms_to_ints)
@string_values Enum.map(@atom_values, &to_string/1)

@impl Ash.Type
def storage_type, do: :integer

@impl Ash.Type
def cast_input(nil, _), do: {:ok, nil}

def cast_input(value, _) when value in @atom_values, do: {:ok, value}
def cast_input(value, _) when value in @string_values, do: {:ok, String.to_existing_atom(value)}

def cast_input(integer, _) when is_integer(integer),
do: Map.fetch(@ints_to_atoms, integer)

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

@impl Ash.Type
def matches_type?(value, _) when is_atom(value) and value in @atom_values, do: true
def matches_type?(_, _), do: false

@impl Ash.Type
def cast_stored(nil, _), do: {:ok, nil}
def cast_stored(integer, _) when is_integer(integer), do: Map.fetch(@ints_to_atoms, integer)
def cast_stored(_, _), do: :error

@impl Ash.Type
def dump_to_native(nil, _), do: {:ok, nil}
def dump_to_native(atom, _) when is_atom(atom), do: Map.fetch(@atoms_to_ints, atom)
def dump_to_native(_, _), do: :error

@impl Ash.Type
def cast_atomic(new_value, constraints) do
if Ash.Expr.expr?(new_value) do
{:atomic, new_value}
else
case cast_input(new_value, constraints) do
{:ok, value} -> {:atomic, value}
{:error, error} -> {:error, error}
end
end
end

@impl Ash.Type
def apply_atomic_constraints(new_value, _constraints) do
{:ok,
Ash.Expr.expr(
if ^new_value in ^@atom_values do
^new_value
else
error(
Ash.Error.Changes.InvalidChanges,
message: "must be one of %{values}",
vars: %{values: ^Enum.join(@atom_values, ", ")}
)
end
)}
end

@impl AshPostgres.Type
def value_to_postgres_default(_, _, value) do
case Map.fetch(@atoms_to_ints, value) do
{:ok, integer} -> {:ok, Integer.to_string(integer)}
:error -> :error
end
end
end
9 changes: 9 additions & 0 deletions test/type_test.exs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
defmodule AshPostgres.Test.TypeTest do
use AshPostgres.RepoCase, async: false
alias AshPostgres.Test.Post
alias AshPostgres.Test.RSVP

require Ash.Query

Expand Down Expand Up @@ -107,4 +108,12 @@
post = Ash.Query.for_read(Post, :with_version_check, version: 1) |> Ash.read!()
refute is_nil(post)
end

test "array expressions work with custom types that map atoms to integers" do

Check failure on line 112 in test/type_test.exs

View workflow job for this annotation

GitHub Actions / ash-ci (15) / mix test

test array expressions work with custom types that map atoms to integers (AshPostgres.Test.TypeTest)

Check failure on line 112 in test/type_test.exs

View workflow job for this annotation

GitHub Actions / ash-ci (14) / mix test

test array expressions work with custom types that map atoms to integers (AshPostgres.Test.TypeTest)

Check failure on line 112 in test/type_test.exs

View workflow job for this annotation

GitHub Actions / ash-ci (16) / mix test

test array expressions work with custom types that map atoms to integers (AshPostgres.Test.TypeTest)
rsvp = RSVP |> Ash.Changeset.for_create(:create, %{response: :accepted}) |> Ash.create!()

updated = rsvp |> Ash.Changeset.for_update(:clear_response, %{}) |> Ash.update!()

assert updated.response == :awaiting
end
end
Loading