From 063bb11102d78b987fbf56187dc10f58e605dde3 Mon Sep 17 00:00:00 2001 From: Matt Beanland Date: Wed, 20 May 2026 21:06:39 +0930 Subject: [PATCH] field via assigned relationship calculation --- .../field_via_assigned_relationship.ex | 46 +++++++ .../field_via_assigned_relationship_test.exs | 123 ++++++++++++++++++ .../resource/instance/access_service.ex | 8 ++ 3 files changed, 177 insertions(+) create mode 100644 lib/diffo/provider/components/calculations/field_via_assigned_relationship.ex create mode 100644 test/provider/extension/field_via_assigned_relationship_test.exs diff --git a/lib/diffo/provider/components/calculations/field_via_assigned_relationship.ex b/lib/diffo/provider/components/calculations/field_via_assigned_relationship.ex new file mode 100644 index 0000000..6eab486 --- /dev/null +++ b/lib/diffo/provider/components/calculations/field_via_assigned_relationship.ex @@ -0,0 +1,46 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.Provider.Calculations.FieldViaAssignedRelationship do + @moduledoc false + use Ash.Resource.Calculation + + @impl true + def load(_query, _opts, _context), do: [] + + @impl true + def calculate(records, opts, _context) do + via = opts[:via] + field = opts[:field] + + Enum.map(records, fn record -> + record.id + |> traverse(via) + |> Enum.flat_map(fn source_id -> + Diffo.Provider.Instance + |> Ash.Query.filter_input(id: source_id) + |> Ash.read!(domain: Diffo.Provider) + |> Enum.map(&Map.get(&1, field)) + end) + end) + end + + defp traverse(id, nil) do + Diffo.Provider.AssignmentRelationship + |> Ash.Query.filter_input(target_id: id) + |> Ash.read!(domain: Diffo.Provider) + |> Enum.map(& &1.source_id) + end + + defp traverse(id, via) do + Enum.reduce(via, [id], fn alias_step, ids -> + Enum.flat_map(ids, fn i -> + Diffo.Provider.AssignmentRelationship + |> Ash.Query.filter_input(target_id: i, alias: alias_step) + |> Ash.read!(domain: Diffo.Provider) + |> Enum.map(& &1.source_id) + end) + end) + end +end diff --git a/test/provider/extension/field_via_assigned_relationship_test.exs b/test/provider/extension/field_via_assigned_relationship_test.exs new file mode 100644 index 0000000..907bfa2 --- /dev/null +++ b/test/provider/extension/field_via_assigned_relationship_test.exs @@ -0,0 +1,123 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.Provider.Extension.FieldViaAssignedRelationshipTest do + @moduledoc false + use ExUnit.Case, async: true + @moduletag :domain_extended + + alias Diffo.Provider.Assignment + alias Diffo.Test.Servo + + setup do + AshNeo4j.Sandbox.checkout() + on_exit(&AshNeo4j.Sandbox.rollback/0) + end + + defp setup_card(name) do + updates = [ + card: [family: :ISAM, model: "EBLT48", technology: :adsl2Plus], + ports: [first: 1, last: 48, assignable_type: "ADSL2+"] + ] + + {:ok, card} = Servo.build_card(%{name: name}) + {:ok, card} = Servo.define_card(card, %{characteristic_value_updates: updates}) + {:ok, card} = Servo.lifecycle_card(card, %{resource_state: :operating}) + card + end + + describe "FieldViaAssignedRelationship — aliased via" do + test "returns field from source instance reached via alias" do + card = setup_card("cvc-01") + {:ok, service} = Servo.build_access_service(%{}) + + {:ok, _card} = + Servo.assign_port(card, %{ + assignment: %Assignment{ + assignee_id: service.id, + operation: :auto_assign, + alias: :primary + } + }) + + service = Ash.load!(service, [:assigner_name], domain: Servo) + + assert service.assigner_name == ["cvc-01"] + end + + test "returns empty list when no assignment exists" do + {:ok, service} = Servo.build_access_service(%{}) + + service = Ash.load!(service, [:assigner_name], domain: Servo) + + assert service.assigner_name == [] + end + + test "alias filters to only the matching source" do + card_a = setup_card("cvc-01") + card_b = setup_card("cvc-02") + {:ok, service} = Servo.build_access_service(%{}) + + {:ok, _card_a} = + Servo.assign_port(card_a, %{ + assignment: %Assignment{ + assignee_id: service.id, + operation: :auto_assign, + alias: :primary + } + }) + + {:ok, _card_b} = + Servo.assign_port(card_b, %{ + assignment: %Assignment{ + assignee_id: service.id, + operation: :auto_assign, + alias: :secondary + } + }) + + service = Ash.load!(service, [:assigner_name], domain: Servo) + + assert service.assigner_name == ["cvc-01"] + end + end + + describe "FieldViaAssignedRelationship — unaliased (all assigners)" do + test "returns fields from all source instances regardless of alias" do + card_a = setup_card("cvc-01") + card_b = setup_card("cvc-02") + {:ok, service} = Servo.build_access_service(%{}) + + {:ok, _card_a} = + Servo.assign_port(card_a, %{ + assignment: %Assignment{ + assignee_id: service.id, + operation: :auto_assign, + alias: :primary + } + }) + + {:ok, _card_b} = + Servo.assign_port(card_b, %{ + assignment: %Assignment{ + assignee_id: service.id, + operation: :auto_assign, + alias: :secondary + } + }) + + service = Ash.load!(service, [:assigner_names], domain: Servo) + + assert Enum.sort(service.assigner_names) == ["cvc-01", "cvc-02"] + end + + test "returns empty list when no assignments exist" do + {:ok, service} = Servo.build_access_service(%{}) + + service = Ash.load!(service, [:assigner_names], domain: Servo) + + assert service.assigner_names == [] + end + end +end diff --git a/test/support/resource/instance/access_service.ex b/test/support/resource/instance/access_service.ex index 9f102e5..defe2ab 100644 --- a/test/support/resource/instance/access_service.ex +++ b/test/support/resource/instance/access_service.ex @@ -39,6 +39,14 @@ defmodule Diffo.Test.Instance.AccessService do end end + calculations do + calculate :assigner_name, {:array, :string}, + {Diffo.Provider.Calculations.FieldViaAssignedRelationship, [via: [:primary], field: :name]} + + calculate :assigner_names, {:array, :string}, + {Diffo.Provider.Calculations.FieldViaAssignedRelationship, [field: :name]} + end + actions do create :build do accept [:id, :name, :type]