Skip to content

Commit 063bb11

Browse files
committed
field via assigned relationship calculation
1 parent 0d7880b commit 063bb11

3 files changed

Lines changed: 177 additions & 0 deletions

File tree

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# SPDX-FileCopyrightText: 2025 diffo contributors <https://github.com/diffo-dev/diffo/graphs.contributors>
2+
#
3+
# SPDX-License-Identifier: MIT
4+
5+
defmodule Diffo.Provider.Calculations.FieldViaAssignedRelationship do
6+
@moduledoc false
7+
use Ash.Resource.Calculation
8+
9+
@impl true
10+
def load(_query, _opts, _context), do: []
11+
12+
@impl true
13+
def calculate(records, opts, _context) do
14+
via = opts[:via]
15+
field = opts[:field]
16+
17+
Enum.map(records, fn record ->
18+
record.id
19+
|> traverse(via)
20+
|> Enum.flat_map(fn source_id ->
21+
Diffo.Provider.Instance
22+
|> Ash.Query.filter_input(id: source_id)
23+
|> Ash.read!(domain: Diffo.Provider)
24+
|> Enum.map(&Map.get(&1, field))
25+
end)
26+
end)
27+
end
28+
29+
defp traverse(id, nil) do
30+
Diffo.Provider.AssignmentRelationship
31+
|> Ash.Query.filter_input(target_id: id)
32+
|> Ash.read!(domain: Diffo.Provider)
33+
|> Enum.map(& &1.source_id)
34+
end
35+
36+
defp traverse(id, via) do
37+
Enum.reduce(via, [id], fn alias_step, ids ->
38+
Enum.flat_map(ids, fn i ->
39+
Diffo.Provider.AssignmentRelationship
40+
|> Ash.Query.filter_input(target_id: i, alias: alias_step)
41+
|> Ash.read!(domain: Diffo.Provider)
42+
|> Enum.map(& &1.source_id)
43+
end)
44+
end)
45+
end
46+
end
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
# SPDX-FileCopyrightText: 2025 diffo contributors <https://github.com/diffo-dev/diffo/graphs.contributors>
2+
#
3+
# SPDX-License-Identifier: MIT
4+
5+
defmodule Diffo.Provider.Extension.FieldViaAssignedRelationshipTest do
6+
@moduledoc false
7+
use ExUnit.Case, async: true
8+
@moduletag :domain_extended
9+
10+
alias Diffo.Provider.Assignment
11+
alias Diffo.Test.Servo
12+
13+
setup do
14+
AshNeo4j.Sandbox.checkout()
15+
on_exit(&AshNeo4j.Sandbox.rollback/0)
16+
end
17+
18+
defp setup_card(name) do
19+
updates = [
20+
card: [family: :ISAM, model: "EBLT48", technology: :adsl2Plus],
21+
ports: [first: 1, last: 48, assignable_type: "ADSL2+"]
22+
]
23+
24+
{:ok, card} = Servo.build_card(%{name: name})
25+
{:ok, card} = Servo.define_card(card, %{characteristic_value_updates: updates})
26+
{:ok, card} = Servo.lifecycle_card(card, %{resource_state: :operating})
27+
card
28+
end
29+
30+
describe "FieldViaAssignedRelationship — aliased via" do
31+
test "returns field from source instance reached via alias" do
32+
card = setup_card("cvc-01")
33+
{:ok, service} = Servo.build_access_service(%{})
34+
35+
{:ok, _card} =
36+
Servo.assign_port(card, %{
37+
assignment: %Assignment{
38+
assignee_id: service.id,
39+
operation: :auto_assign,
40+
alias: :primary
41+
}
42+
})
43+
44+
service = Ash.load!(service, [:assigner_name], domain: Servo)
45+
46+
assert service.assigner_name == ["cvc-01"]
47+
end
48+
49+
test "returns empty list when no assignment exists" do
50+
{:ok, service} = Servo.build_access_service(%{})
51+
52+
service = Ash.load!(service, [:assigner_name], domain: Servo)
53+
54+
assert service.assigner_name == []
55+
end
56+
57+
test "alias filters to only the matching source" do
58+
card_a = setup_card("cvc-01")
59+
card_b = setup_card("cvc-02")
60+
{:ok, service} = Servo.build_access_service(%{})
61+
62+
{:ok, _card_a} =
63+
Servo.assign_port(card_a, %{
64+
assignment: %Assignment{
65+
assignee_id: service.id,
66+
operation: :auto_assign,
67+
alias: :primary
68+
}
69+
})
70+
71+
{:ok, _card_b} =
72+
Servo.assign_port(card_b, %{
73+
assignment: %Assignment{
74+
assignee_id: service.id,
75+
operation: :auto_assign,
76+
alias: :secondary
77+
}
78+
})
79+
80+
service = Ash.load!(service, [:assigner_name], domain: Servo)
81+
82+
assert service.assigner_name == ["cvc-01"]
83+
end
84+
end
85+
86+
describe "FieldViaAssignedRelationship — unaliased (all assigners)" do
87+
test "returns fields from all source instances regardless of alias" do
88+
card_a = setup_card("cvc-01")
89+
card_b = setup_card("cvc-02")
90+
{:ok, service} = Servo.build_access_service(%{})
91+
92+
{:ok, _card_a} =
93+
Servo.assign_port(card_a, %{
94+
assignment: %Assignment{
95+
assignee_id: service.id,
96+
operation: :auto_assign,
97+
alias: :primary
98+
}
99+
})
100+
101+
{:ok, _card_b} =
102+
Servo.assign_port(card_b, %{
103+
assignment: %Assignment{
104+
assignee_id: service.id,
105+
operation: :auto_assign,
106+
alias: :secondary
107+
}
108+
})
109+
110+
service = Ash.load!(service, [:assigner_names], domain: Servo)
111+
112+
assert Enum.sort(service.assigner_names) == ["cvc-01", "cvc-02"]
113+
end
114+
115+
test "returns empty list when no assignments exist" do
116+
{:ok, service} = Servo.build_access_service(%{})
117+
118+
service = Ash.load!(service, [:assigner_names], domain: Servo)
119+
120+
assert service.assigner_names == []
121+
end
122+
end
123+
end

test/support/resource/instance/access_service.ex

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,14 @@ defmodule Diffo.Test.Instance.AccessService do
3939
end
4040
end
4141

42+
calculations do
43+
calculate :assigner_name, {:array, :string},
44+
{Diffo.Provider.Calculations.FieldViaAssignedRelationship, [via: [:primary], field: :name]}
45+
46+
calculate :assigner_names, {:array, :string},
47+
{Diffo.Provider.Calculations.FieldViaAssignedRelationship, [field: :name]}
48+
end
49+
4250
actions do
4351
create :build do
4452
accept [:id, :name, :type]

0 commit comments

Comments
 (0)