Skip to content

Commit 70de718

Browse files
committed
ash_neo4j 0.6.0 upgrade + domain extension pattern
1 parent b57e0b6 commit 70de718

44 files changed

Lines changed: 219 additions & 16 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

AGENTS.md

Lines changed: 68 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -173,15 +173,72 @@ provider do
173173
end
174174
```
175175

176+
## Three usage scenarios
177+
178+
Diffo supports three distinct usage patterns. Every test is tagged with one or more of these
179+
atoms — absence of all three means the test has not yet been classified.
180+
181+
| Tag | Scenario | Description |
182+
|-----|----------|-------------|
183+
| `:provider_only` | Vanilla Provider | Uses `Diffo.Provider` resources as-is. No custom domains, no extensions. Good for basic TMF inventory and for introducing Diffo incrementally. |
184+
| `:provider_extended` | Extended within Provider | New resource types defined inside `Diffo.Provider` itself, extending base fragments (e.g. `DefinedSimpleRelationship`). Pain point: external users can't add to the Provider domain without forking Diffo. |
185+
| `:domain_extended` | True domain extension | The **recommended pattern**. An external domain (e.g. `MyApp.SRM`) owns resources using `BaseInstance`, `BaseParty`, `BasePlace`, and `BaseCharacteristic` fragments. Exposes its own API; consumers need not know about Diffo internals. |
186+
187+
Tests may carry `:provider_extended` and `:domain_extended` together when they span both.
188+
`:provider_only` is mutually exclusive with the other two.
189+
190+
## Domain extension pattern (scenario 3)
191+
192+
Any domain whose resources carry `belongs_to :instance, Diffo.Provider.Instance` (or
193+
`belongs_to :party, Diffo.Provider.Party`) and use `manage_relationship` to relate them
194+
**must** include `Diffo.Provider.DomainFragment`:
195+
196+
```elixir
197+
defmodule MyApp.SRM do
198+
use Ash.Domain, fragments: [Diffo.Provider.DomainFragment]
199+
...
200+
end
201+
```
202+
203+
**Why this is necessary.** AshNeo4j 0.6.0 matches nodes using
204+
`label_pair = [domain_label, module_label]`. `Ash.get(Diffo.Provider.Instance, uuid)` builds
205+
`MATCH (n:Provider:Instance {uuid: $uuid})`. A `ShelfInstance` node in `MyApp.SRM` has
206+
labels `[:SRM, :ShelfInstance, :Instance]``:Provider` is absent, so the lookup returns
207+
not-found and `manage_relationship` fails.
208+
209+
`Diffo.Provider.DomainFragment` tells AshNeo4j to write `:Provider` as an extra label on
210+
every node in the domain at CREATE time. `ShelfInstance` then carries
211+
`[:SRM, :ShelfInstance, :Instance, :Provider]`. Neo4j matches nodes that have **all**
212+
specified labels regardless of extras, so `MATCH (n:Provider:Instance {uuid: $uuid})` finds
213+
it. `label_pair` for direct reads on `ShelfInstance` is still `[:SRM, :ShelfInstance]`
214+
its own-domain reads remain correctly scoped.
215+
216+
### has_many and the accessing_from path
217+
218+
A separate constraint applies when a `has_many` relationship uses `manage_relationship` on
219+
the source side: AshNeo4j 0.6.0's `accessing_from` path calls
220+
`Ash.Resource.Info.reverse_relationship/2`, which does a strict type-equality check. If
221+
`Characteristic.belongs_to :instance` targets `Diffo.Provider.Instance` but the actual
222+
source is `ShelfInstance`, the check fails and the edge is not created.
223+
224+
The fix used in Diffo's extension helpers (`Characteristic.relate_instance`,
225+
`Feature.relate_instance`) is to bypass `manage_relationship` on the source side entirely
226+
and call `AshNeo4j.Neo4jHelper.relate_nodes/6` directly, using the concrete
227+
`result.__struct__` label pair. See
228+
`lib/diffo/provider/components/instance/extension/characteristic.ex` and `feature.ex`.
229+
176230
## Running tests
177231

178232
Integration tests require a running Neo4j instance.
179233

180234
```sh
181-
mix test # full suite
182-
mix test test/provider/extension/ # extension tests only
183-
mix test path/to/test.exs:LINE # single test
184-
mix test --max-failures 5 # stop early
235+
mix test # full suite
236+
mix test --only domain_extended # scenario 3 tests only
237+
mix test --only provider_only # vanilla provider tests only
238+
mix test --only provider_extended # extended-within-provider tests only
239+
mix test test/provider/extension/ # extension directory only
240+
mix test path/to/test.exs:LINE # single test
241+
mix test --max-failures 5 # stop early
185242
```
186243

187244
## Module naming and Neo4j labels
@@ -256,3 +313,10 @@ not. Add any useful hypotheses as a follow-up comment on the issue, then leave i
256313
Run `mix format` afterward to verify.
257314
- Editing content between `<!-- usage-rules-start -->` markers in `CLAUDE.md` — that is
258315
auto-generated by `mix usage_rules.sync`.
316+
- Forgetting `Diffo.Provider.DomainFragment` on a scenario 3 domain — any domain whose
317+
resources relate back to Provider base types (`belongs_to :instance, Diffo.Provider.Instance`
318+
etc.) via `manage_relationship` will get `Ash.Error.Query.NotFound` at runtime without it.
319+
See the **Domain extension pattern** section above.
320+
- Bypassing `manage_relationship` by replacing `argument + manage_relationship` with bare
321+
`accept` for relationship IDs in scenario 3 resources — the correct fix is the domain
322+
fragment, not removing the relationship management.

lib/diffo/provider/components/party_ref.ex

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,17 @@ defmodule Diffo.Provider.PartyRef do
5151

5252
create :create do
5353
description "creates a party ref, relating an instance, place or source party to a party"
54-
# IDs accepted directly as attributes so AshNeo4j's create_from_attributes path
55-
# builds graph edges using the single labels in the relate DSL (:Instance, :Party, :Place).
56-
# manage_relationship would fail: it looks up the generic Diffo.Provider.Instance/Party
57-
# by label_pair, which doesn't match domain-specific subtypes (ShelfInstance, Person, etc.).
58-
accept [:role, :instance_id, :place_id, :source_party_id, :party_id]
54+
accept [:role]
55+
56+
argument :instance_id, :uuid
57+
argument :place_id, :string
58+
argument :source_party_id, :string
59+
argument :party_id, :string
60+
61+
change manage_relationship(:instance_id, :instance, type: :append_and_remove)
62+
change manage_relationship(:place_id, :place, type: :append_and_remove)
63+
change manage_relationship(:source_party_id, :source_party, type: :append_and_remove)
64+
change manage_relationship(:party_id, :party, type: :append_and_remove)
5965
end
6066

6167
read :list do
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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.DomainFragment do
6+
@moduledoc """
7+
Domain fragment for Ash domains that extend the Diffo Provider.
8+
9+
Include this fragment in any domain whose resources need to participate in provider
10+
polymorphism — i.e., where `belongs_to :instance, Diffo.Provider.Instance` or
11+
`belongs_to :party, Diffo.Provider.Party` relationships must resolve via `manage_relationship`.
12+
13+
Adding this fragment causes AshNeo4j to write `:Provider` as an additional label on every
14+
node in the domain at CREATE time. Because AshNeo4j MATCH patterns include all node labels,
15+
`Ash.get(Diffo.Provider.Instance, uuid)` (which matches on `[:Provider, :Instance]`) will
16+
then find concrete instance nodes (e.g. `ShelfInstance`) that carry both `:Instance` (from
17+
`BaseInstance`) and `:Provider` (from this fragment).
18+
19+
## Usage
20+
21+
defmodule MyApp.SRM do
22+
use Ash.Domain, fragments: [Diffo.Provider.DomainFragment]
23+
...
24+
end
25+
"""
26+
use Spark.Dsl.Fragment,
27+
of: Ash.Domain,
28+
extensions: [AshNeo4j.DataLayer.Domain]
29+
30+
neo4j do
31+
label :Provider
32+
end
33+
end

test/diffo_test.exs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
defmodule DiffoTest do
66
@moduledoc false
77
use ExUnit.Case, async: true
8+
@moduletag :provider_only
89
doctest Diffo
910
doctest Diffo.Unwrap
1011
doctest Diffo.Type.Primitive

test/provider/characteristic_test.exs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
defmodule Diffo.Provider.CharacteristicTest do
66
@moduledoc false
77
use ExUnit.Case, async: true
8+
@moduletag :provider_only
89
alias Diffo.Test.Patch
910
alias Diffo.Type.Value
1011

test/provider/defined_simple_relationship_test.exs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
defmodule Diffo.Provider.DefinedSimpleRelationshipTest do
66
@moduledoc false
77
use ExUnit.Case, async: true
8+
@moduletag :provider_extended
89

910
alias Diffo.Type.NameValuePrimitive
1011
alias Diffo.Type.Primitive

test/provider/entity_ref_test.exs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
defmodule Diffo.Provider.EntityRefTest do
66
@moduledoc false
77
use ExUnit.Case, async: true
8+
@moduletag :provider_only
89
use Outstand
910
alias Diffo.Provider.Entity
1011
alias Diffo.Provider.EntityRef

test/provider/entity_test.exs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
defmodule Diffo.Provider.EntityTest do
66
@moduledoc false
77
use ExUnit.Case, async: true
8+
@moduletag :provider_only
89
use Outstand
910

1011
setup do

test/provider/event_test.exs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
defmodule Diffo.Provider.EventTest do
66
@moduledoc false
77
use ExUnit.Case, async: true
8+
@moduletag :provider_only
89

910
setup do
1011
AshNeo4j.Sandbox.checkout()

test/provider/extension/assigner_test.exs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
defmodule Diffo.Provider.Extension.AssignerTest do
66
@moduledoc false
77
use ExUnit.Case, async: true
8+
@moduletag :domain_extended
89
alias Diffo.Provider.Specification
910
alias Diffo.Provider.Characteristic
1011
alias Diffo.Provider.Assignment

0 commit comments

Comments
 (0)