From d551ff973fb1d3e3163fc47c234e105d06575d80 Mon Sep 17 00:00:00 2001 From: Matt Beanland Date: Mon, 27 Apr 2026 00:40:09 +0930 Subject: [PATCH] refactor RSP as a BaseParty fragment with EPID as Party id --- .../domains/diffo_example_nbn.livemd | 10 ++- lib/nbn/initializer.ex | 18 ++--- lib/nbn/nbn.ex | 6 +- lib/nbn/resources/avc.ex | 2 +- lib/nbn/resources/cvc.ex | 2 +- lib/nbn/resources/nbn_ethernet.ex | 2 +- lib/nbn/resources/nni.ex | 2 +- lib/nbn/resources/nni_group.ex | 2 +- lib/nbn/resources/rsp.ex | 70 ++++++------------- mix.exs | 4 +- mix.lock | 2 +- test/nbn/rsp_test.exs | 24 +++---- 12 files changed, 61 insertions(+), 83 deletions(-) diff --git a/documentation/domains/diffo_example_nbn.livemd b/documentation/domains/diffo_example_nbn.livemd index df77a32..560ce49 100644 --- a/documentation/domains/diffo_example_nbn.livemd +++ b/documentation/domains/diffo_example_nbn.livemd @@ -9,7 +9,9 @@ SPDX-License-Identifier: MIT ```elixir Mix.install( [ - {:diffo_example, "~> 0.2.0"}, + # {:diffo_example, "~> 0.3.0"}, + {:diffo_example, github: "diffo-dev/diffo_example", branch: "dev"}, + {:diffo, github: "diffo-dev/diffo", branch: "dev", override: true}, {:kino, "~> 0.14"}, {:req, "~> 0.5"} ], @@ -117,7 +119,9 @@ Speeds.speeds(:home_fast, :FixedWireless) ## Multi-tenancy -Each RSP operates in isolation — they can only see and manage the resources they own. This multi-tenancy is enforced at the Ash policy layer: every NBN resource is stamped with the owning RSP's id at creation, and subsequent reads, updates, and destroys are scoped to the record owner. +Each RSP operates in isolation — they can only see and manage the resources they own. This multi-tenancy is enforced at the Ash policy layer: every NBN resource is stamped with the owning RSP's EPID at creation, and subsequent reads, updates, and destroys are scoped to the record owner. + +RSP is modelled as a Party (using the `Diffo.Provider.BaseParty` fragment), with its EPID as the Party id. This means the `rsp_id` stamped on owned resources is a human-readable four-digit identifier rather than a UUID. Select the RSP you want to operate as for the rest of this livebook. All resources you build will be owned by that RSP and isolated from resources owned by others. @@ -127,7 +131,7 @@ alias DiffoExample.Nbn.Rsp import Jason, only: [encode: 2] DiffoExample.Nbn.Initializer.init() rsps = Nbn.list_rsps!() -Kino.DataTable.new(rsps, keys: [:epid, :name, :short_name, :state]) +Kino.DataTable.new(rsps, keys: [:id, :name, :short_name, :state]) ``` ```elixir diff --git a/lib/nbn/initializer.ex b/lib/nbn/initializer.ex index 13b80e5..5b20ea8 100644 --- a/lib/nbn/initializer.ex +++ b/lib/nbn/initializer.ex @@ -15,13 +15,13 @@ defmodule DiffoExample.Nbn.Initializer do alias DiffoExample.Nbn @rsps [ - %{name: "Wedge-tail Telecom", short_name: :wedgetail, epid: "0001"}, - %{name: "Quokka Connect", short_name: :quokka, epid: "0002"}, - %{name: "Ibis Telecom", short_name: :ibis, epid: "0003"}, - %{name: "Taipan Group", short_name: :taipan, epid: "0004"}, - %{name: "Echidna Networks", short_name: :echidna, epid: "0005"}, - %{name: "Dugong Digital", short_name: :dugong, epid: "0006"}, - %{name: "Lyrebird", short_name: :lyrebird, epid: "0007"} + %{name: "Wedge-tail Telecom", short_name: :wedgetail, id: "0001"}, + %{name: "Quokka Connect", short_name: :quokka, id: "0002"}, + %{name: "Ibis Telecom", short_name: :ibis, id: "0003"}, + %{name: "Taipan Group", short_name: :taipan, id: "0004"}, + %{name: "Echidna Networks", short_name: :echidna, id: "0005"}, + %{name: "Dugong Digital", short_name: :dugong, id: "0006"}, + %{name: "Lyrebird", short_name: :lyrebird, id: "0007"} ] def init do @@ -41,13 +41,13 @@ defmodule DiffoExample.Nbn.Initializer do defp seed_rsps do Enum.each(@rsps, fn attrs -> try do - case Nbn.get_rsp_by_epid(attrs.epid) do + case Nbn.get_rsp_by_epid(attrs.id) do {:ok, nil} -> seed_rsp(attrs) {:ok, _} -> :ok {:error, _} -> seed_rsp(attrs) end rescue - e -> require Logger; Logger.error("Exception seeding RSP #{attrs.epid}: #{inspect(e)}") + e -> require Logger; Logger.error("Exception seeding RSP #{attrs.id}: #{inspect(e)}") end end) end diff --git a/lib/nbn/nbn.ex b/lib/nbn/nbn.ex index 43d4bfa..f377ff0 100644 --- a/lib/nbn/nbn.ex +++ b/lib/nbn/nbn.ex @@ -163,10 +163,10 @@ defmodule DiffoExample.Nbn do end resource Rsp do - define :list_rsps, action: :list - define :get_rsp_by_epid, action: :read, get_by: :epid + define :list_rsps, action: :inventory + define :get_rsp_by_epid, action: :read, get_by: :id define :get_rsp_by_short_name, action: :read, get_by: :short_name - define :create_rsp, action: :create + define :create_rsp, action: :build define :activate_rsp, action: :activate define :suspend_rsp, action: :suspend define :deactivate_rsp, action: :deactivate diff --git a/lib/nbn/resources/avc.ex b/lib/nbn/resources/avc.ex index 5f49871..9b8e25c 100644 --- a/lib/nbn/resources/avc.ex +++ b/lib/nbn/resources/avc.ex @@ -48,7 +48,7 @@ defmodule DiffoExample.Nbn.Avc do end attributes do - attribute :rsp_id, :uuid do + attribute :rsp_id, :string do description "the owning RSP's id — nil for Perentie-managed infrastructure" allow_nil? true public? true diff --git a/lib/nbn/resources/cvc.ex b/lib/nbn/resources/cvc.ex index e6b8215..8c27b4c 100644 --- a/lib/nbn/resources/cvc.ex +++ b/lib/nbn/resources/cvc.ex @@ -52,7 +52,7 @@ defmodule DiffoExample.Nbn.Cvc do end attributes do - attribute :rsp_id, :uuid do + attribute :rsp_id, :string do description "the owning RSP's id — nil for Perentie-managed infrastructure" allow_nil? true public? true diff --git a/lib/nbn/resources/nbn_ethernet.ex b/lib/nbn/resources/nbn_ethernet.ex index d5c2ec2..d9657d5 100644 --- a/lib/nbn/resources/nbn_ethernet.ex +++ b/lib/nbn/resources/nbn_ethernet.ex @@ -52,7 +52,7 @@ defmodule DiffoExample.Nbn.NbnEthernet do end attributes do - attribute :rsp_id, :uuid do + attribute :rsp_id, :string do description "the owning RSP's id — nil for Perentie-managed infrastructure" allow_nil? true public? true diff --git a/lib/nbn/resources/nni.ex b/lib/nbn/resources/nni.ex index a3682ea..faca634 100644 --- a/lib/nbn/resources/nni.ex +++ b/lib/nbn/resources/nni.ex @@ -48,7 +48,7 @@ defmodule DiffoExample.Nbn.Nni do end attributes do - attribute :rsp_id, :uuid do + attribute :rsp_id, :string do description "the owning RSP's id — nil for Perentie-managed infrastructure" allow_nil? true public? true diff --git a/lib/nbn/resources/nni_group.ex b/lib/nbn/resources/nni_group.ex index 7f60ac1..dacc6d7 100644 --- a/lib/nbn/resources/nni_group.ex +++ b/lib/nbn/resources/nni_group.ex @@ -51,7 +51,7 @@ defmodule DiffoExample.Nbn.NniGroup do end attributes do - attribute :rsp_id, :uuid do + attribute :rsp_id, :string do description "the owning RSP's id — nil for Perentie-managed infrastructure" allow_nil? true public? true diff --git a/lib/nbn/resources/rsp.ex b/lib/nbn/resources/rsp.ex index 1f4fd66..f64cd0e 100644 --- a/lib/nbn/resources/rsp.ex +++ b/lib/nbn/resources/rsp.ex @@ -11,29 +11,31 @@ defmodule DiffoExample.Nbn.Rsp do An RSP is a licensed provider operating within the Perentie ecosystem. Each RSP is assigned an EPID (four-digit regulator-assigned identifier) and a short_name atom used as their actor identity for authorisation. + + RSP is a Party of kind :organization. The EPID is used as the Party id (Neo4j key). """ alias DiffoExample.Nbn use Ash.Resource, domain: Nbn, - data_layer: AshNeo4j.DataLayer, authorizers: [Ash.Policy.Authorizer], - extensions: [AshStateMachine, AshJason.Resource, AshJsonApi.Resource] - - neo4j do - label :Rsp - end + extensions: [AshStateMachine, AshJsonApi.Resource], + fragments: [Diffo.Provider.BaseParty] + + # BaseParty provides: + # data_layer: AshNeo4j.DataLayer + # extensions: AshJason.Resource, AshOutstanding.Resource, Diffo.Provider.Party.Extension + # Neo4j label :Party (RSP nodes are Party nodes) + # attributes: id (string/key), name, kind, created_at, updated_at + # relationships: party_refs + # actions: :read (primary), :destroy, :create (accept [:id,:name,:kind]), + # :update (name), :list (unsorted), :find_by_name json_api do type "rsp" end - jason do - pick [:id, :name, :short_name, :epid, :state] - compact true - end - state_machine do initial_states [:inactive] default_initial_state :inactive @@ -47,59 +49,32 @@ defmodule DiffoExample.Nbn.Rsp do end attributes do - attribute :id, :uuid do - primary_key? true - allow_nil? false - public? true - default &Ash.UUID.generate/0 - source :uuid - end - - attribute :name, :string do - description "the RSP's registered trading name" - allow_nil? false - public? true - end - attribute :short_name, :atom do description "atom identifier used as the actor for authorisation" allow_nil? false public? true end - attribute :epid, :string do - description "four-digit regulator-assigned provider identifier, in historical sequence" - allow_nil? false - public? true - constraints [match: ~r/^\d{4}$/] - end - attribute :state, :atom do allow_nil? false default :inactive public? true constraints [one_of: [:active, :suspended, :inactive]] end - - create_timestamp :created_at - update_timestamp :updated_at end actions do - defaults [:destroy] - - read :read do - primary? true - end - - read :list do - prepare build(sort: [epid: :asc]) + create :build do + accept [:name, :short_name, :id] + upsert? true + change set_attribute(:kind, :organization) + validate match(:id, ~r/^\d{4}$/) do + message "must be a four-digit EPID" + end end - create :create do - accept [:name, :short_name, :epid] - upsert? true - upsert_identity :unique_epid + read :inventory do + prepare build(sort: [id: :asc]) end update :activate do @@ -119,7 +94,6 @@ defmodule DiffoExample.Nbn.Rsp do end identities do - identity :unique_epid, [:epid] identity :unique_name, [:name] identity :unique_short_name, [:short_name] end diff --git a/mix.exs b/mix.exs index 31c6573..9db20e5 100644 --- a/mix.exs +++ b/mix.exs @@ -45,7 +45,7 @@ defmodule DiffoExample.MixProject do nil -> default_version "local" -> [path: "../diffo"] "main" -> [git: "https://github.com/diffo-dev/diffo.git"] - "0.2.0" -> [git: "https://github.com/diffo-dev/diffo.git", tag: "v0.2.0"] + "dev" -> [git: "https://github.com/diffo-dev/diffo.git", branch: "dev"] version -> "~> #{version}" end end @@ -86,7 +86,7 @@ defmodule DiffoExample.MixProject do # Run "mix help deps" to learn about dependencies. defp deps do [ - {:diffo, diffo_version("~> 0.2.0")}, + {:diffo, diffo_version([git: "https://github.com/diffo-dev/diffo.git", branch: "dev"])}, {:ash_json_api, "~> 1.6"}, {:plug_cowboy, "~> 2.7"}, {:req, "~> 0.5", only: [:dev, :test]}, diff --git a/mix.lock b/mix.lock index 136c6c6..03e771e 100644 --- a/mix.lock +++ b/mix.lock @@ -13,7 +13,7 @@ "crux": {:hex, :crux, "0.1.2", "4441c9e3a34f1e340954ce96b9ad5a2de13ceb4f97b3f910211227bb92e2ca90", [:mix], [{:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:stream_data, "~> 1.0", [hex: :stream_data, repo: "hexpm", optional: true]}], "hexpm", "563ea3748ebfba9cc078e6d198a1d6a06015a8fae503f0b721363139f0ddb350"}, "db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"}, "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"}, - "diffo": {:hex, :diffo, "0.2.0", "ac07bb5ea92d765601fba3e61e8a5dac5c3c7f18b3a55bcf3019a574fda03d65", [:mix], [{:ash, ">= 3.24.2 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_jason, "~> 3.0", [hex: :ash_jason, repo: "hexpm", optional: false]}, {:ash_neo4j, "~> 0.3.1", [hex: :ash_neo4j, repo: "hexpm", optional: false]}, {:ash_outstanding, "~> 0.2.3", [hex: :ash_outstanding, repo: "hexpm", optional: false]}, {:ash_state_machine, "~> 0.2.12", [hex: :ash_state_machine, repo: "hexpm", optional: false]}, {:igniter, ">= 0.6.29 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:uuid, "~> 1.1", [hex: :uuid, repo: "hexpm", optional: false]}], "hexpm", "2a140d9e427e30b06b29a04eeafec8b98d7acfeaffdbfa06cf6c152998302503"}, + "diffo": {:git, "https://github.com/diffo-dev/diffo.git", "5197a374b64ceeb70783da72e2d6a380f9b9d510", [branch: "dev"]}, "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, "ecto": {:hex, :ecto, "3.13.5", "9d4a69700183f33bf97208294768e561f5c7f1ecf417e0fa1006e4a91713a834", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "df9efebf70cf94142739ba357499661ef5dbb559ef902b68ea1f3c1fabce36de"}, "elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"}, diff --git a/test/nbn/rsp_test.exs b/test/nbn/rsp_test.exs index e009225..a74f452 100644 --- a/test/nbn/rsp_test.exs +++ b/test/nbn/rsp_test.exs @@ -26,11 +26,11 @@ defmodule DiffoExample.Nbn.RspTest do describe "RSP resource" do test "create and activate an RSP" do - {:ok, rsp} = Nbn.create_rsp(%{name: "Wedge-tail Telecom", short_name: :wedgetail, epid: "8001"}) + {:ok, rsp} = Nbn.create_rsp(%{name: "Wedge-tail Telecom", short_name: :wedgetail, id: "8001"}) assert is_struct(rsp, Rsp) assert rsp.state == :inactive - assert rsp.epid == "8001" + assert rsp.id == "8001" assert rsp.short_name == :wedgetail {:ok, rsp} = Nbn.activate_rsp(rsp) @@ -38,7 +38,7 @@ defmodule DiffoExample.Nbn.RspTest do end test "RSP state machine: activate → suspend → deactivate" do - {:ok, rsp} = Nbn.create_rsp(%{name: "Wedge-tail Telecom", short_name: :wedgetail, epid: "8001"}) + {:ok, rsp} = Nbn.create_rsp(%{name: "Wedge-tail Telecom", short_name: :wedgetail, id: "8001"}) {:ok, rsp} = Nbn.activate_rsp(rsp) assert rsp.state == :active @@ -50,22 +50,22 @@ defmodule DiffoExample.Nbn.RspTest do assert rsp.state == :inactive end - test "epid must be exactly 4 digits" do - assert {:error, _} = Nbn.create_rsp(%{name: "Bad RSP", short_name: :bad, epid: "123"}) - assert {:error, _} = Nbn.create_rsp(%{name: "Bad RSP", short_name: :bad, epid: "12345"}) - assert {:error, _} = Nbn.create_rsp(%{name: "Bad RSP", short_name: :bad, epid: "abcd"}) + test "id must be a four-digit EPID" do + assert {:error, _} = Nbn.create_rsp(%{name: "Bad RSP", short_name: :bad, id: "123"}) + assert {:error, _} = Nbn.create_rsp(%{name: "Bad RSP", short_name: :bad, id: "12345"}) + assert {:error, _} = Nbn.create_rsp(%{name: "Bad RSP", short_name: :bad, id: "abcd"}) end test "get RSP by short_name" do - create_rsp(%{name: "Wedge-tail Telecom", short_name: :wedgetail, epid: "8001"}) + create_rsp(%{name: "Wedge-tail Telecom", short_name: :wedgetail, id: "8001"}) {:ok, rsp} = Nbn.get_rsp_by_short_name(:wedgetail) assert rsp.short_name == :wedgetail - assert rsp.epid == "8001" + assert rsp.id == "8001" end test "get RSP by epid" do - create_rsp(%{name: "Quokka Connect", short_name: :quokka, epid: "8002"}) + create_rsp(%{name: "Quokka Connect", short_name: :quokka, id: "8002"}) {:ok, rsp} = Nbn.get_rsp_by_epid("8002") assert rsp.short_name == :quokka @@ -74,8 +74,8 @@ defmodule DiffoExample.Nbn.RspTest do describe "RSP multi-tenancy" do setup do - wedgetail = create_rsp(%{name: "Wedge-tail Telecom", short_name: :wedgetail, epid: "8001"}) - quokka = create_rsp(%{name: "Quokka Connect", short_name: :quokka, epid: "8002"}) + wedgetail = create_rsp(%{name: "Wedge-tail Telecom", short_name: :wedgetail, id: "8001"}) + quokka = create_rsp(%{name: "Quokka Connect", short_name: :quokka, id: "8002"}) %{wedgetail: wedgetail, quokka: quokka} end