diff --git a/lib/diffo/provider/components/calculations/field_via_aliased_relationship.ex b/lib/diffo/provider/components/calculations/field_via_aliased_relationship.ex new file mode 100644 index 0000000..85f32d1 --- /dev/null +++ b/lib/diffo/provider/components/calculations/field_via_aliased_relationship.ex @@ -0,0 +1,42 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.Provider.Calculations.FieldViaAliasedRelationship do + @moduledoc false + use Ash.Resource.Calculation + + @impl true + def load(_query, _opts, _context), do: [] + + @impl true + def calculate(records, opts, _context) do + alias_name = opts[:alias] + field = opts[:field] + + Enum.map(records, fn record -> + record.id + |> traverse(alias_name) + |> Enum.flat_map(fn target_id -> + Diffo.Provider.Instance + |> Ash.Query.filter_input(id: target_id) + |> Ash.read!(domain: Diffo.Provider) + |> Enum.map(&Map.get(&1, field)) + end) + end) + end + + defp traverse(id, nil) do + Diffo.Provider.DefinedSimpleRelationship + |> Ash.Query.filter_input(source_id: id) + |> Ash.read!(domain: Diffo.Provider) + |> Enum.map(& &1.target_id) + end + + defp traverse(id, alias_name) do + Diffo.Provider.DefinedSimpleRelationship + |> Ash.Query.filter_input(source_id: id, alias: alias_name) + |> Ash.read!(domain: Diffo.Provider) + |> Enum.map(& &1.target_id) + end +end diff --git a/test/provider/extension/field_via_aliased_relationship_test.exs b/test/provider/extension/field_via_aliased_relationship_test.exs new file mode 100644 index 0000000..f58b723 --- /dev/null +++ b/test/provider/extension/field_via_aliased_relationship_test.exs @@ -0,0 +1,101 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.Provider.Extension.FieldViaAliasedRelationshipTest do + @moduledoc false + use ExUnit.Case, async: true + @moduletag :domain_extended + + alias Diffo.Test.Parties + alias Diffo.Test.Servo + + setup do + AshNeo4j.Sandbox.checkout() + on_exit(&AshNeo4j.Sandbox.rollback/0) + end + + describe "FieldViaAliasedRelationship — aliased" do + test "returns field from target instance reached via alias" do + {:ok, shelf} = Parties.build_shelf_with_installer() + {:ok, card} = Servo.build_card(%{name: "target-card"}) + + Diffo.Provider.create_defined_simple_relationship!(%{ + type: :assignedTo, + alias: :link, + source_id: shelf.id, + target_id: card.id + }) + + shelf = Ash.load!(shelf, [:linked_target_name], domain: Servo) + + assert shelf.linked_target_name == ["target-card"] + end + + test "returns empty list when no matching relationship exists" do + {:ok, shelf} = Parties.build_shelf_with_installer() + + shelf = Ash.load!(shelf, [:linked_target_name], domain: Servo) + + assert shelf.linked_target_name == [] + end + + test "alias filters to only the matching target" do + {:ok, shelf} = Parties.build_shelf_with_installer() + {:ok, card_a} = Servo.build_card(%{name: "target-a"}) + {:ok, card_b} = Servo.build_card(%{name: "target-b"}) + + Diffo.Provider.create_defined_simple_relationship!(%{ + type: :assignedTo, + alias: :link, + source_id: shelf.id, + target_id: card_a.id + }) + + Diffo.Provider.create_defined_simple_relationship!(%{ + type: :assignedTo, + alias: :other, + source_id: shelf.id, + target_id: card_b.id + }) + + shelf = Ash.load!(shelf, [:linked_target_name], domain: Servo) + + assert shelf.linked_target_name == ["target-a"] + end + end + + describe "FieldViaAliasedRelationship — unaliased (all targets)" do + test "returns fields from all related target instances regardless of alias" do + {:ok, shelf} = Parties.build_shelf_with_installer() + {:ok, card_a} = Servo.build_card(%{name: "target-a"}) + {:ok, card_b} = Servo.build_card(%{name: "target-b"}) + + Diffo.Provider.create_defined_simple_relationship!(%{ + type: :assignedTo, + alias: :link, + source_id: shelf.id, + target_id: card_a.id + }) + + Diffo.Provider.create_defined_simple_relationship!(%{ + type: :assignedTo, + alias: :other, + source_id: shelf.id, + target_id: card_b.id + }) + + shelf = Ash.load!(shelf, [:all_linked_names], domain: Servo) + + assert Enum.sort(shelf.all_linked_names) == ["target-a", "target-b"] + end + + test "returns empty list when no relationships exist" do + {:ok, shelf} = Parties.build_shelf_with_installer() + + shelf = Ash.load!(shelf, [:all_linked_names], domain: Servo) + + assert shelf.all_linked_names == [] + end + end +end diff --git a/test/support/resource/instance/shelf_instance.ex b/test/support/resource/instance/shelf_instance.ex index fcfbef9..e9fc1f4 100644 --- a/test/support/resource/instance/shelf_instance.ex +++ b/test/support/resource/instance/shelf_instance.ex @@ -84,6 +84,14 @@ defmodule Diffo.Test.Instance.ShelfInstance do end end + calculations do + calculate :linked_target_name, {:array, :string}, + {Diffo.Provider.Calculations.FieldViaAliasedRelationship, [alias: :link, field: :name]} + + calculate :all_linked_names, {:array, :string}, + {Diffo.Provider.Calculations.FieldViaAliasedRelationship, [field: :name]} + end + actions do create :build do description "creates a new Shelf resource instance for build"