Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions documentation/domains/diffo_example_nbn.livemd
Original file line number Diff line number Diff line change
Expand Up @@ -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"}
],
Expand Down Expand Up @@ -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.

Expand All @@ -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
Expand Down
18 changes: 9 additions & 9 deletions lib/nbn/initializer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
6 changes: 3 additions & 3 deletions lib/nbn/nbn.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion lib/nbn/resources/avc.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion lib/nbn/resources/cvc.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion lib/nbn/resources/nbn_ethernet.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion lib/nbn/resources/nni.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion lib/nbn/resources/nni_group.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
70 changes: 22 additions & 48 deletions lib/nbn/resources/rsp.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]},
Expand Down
2 changes: 1 addition & 1 deletion mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
Expand Down
24 changes: 12 additions & 12 deletions test/nbn/rsp_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -26,19 +26,19 @@ 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)
assert rsp.state == :active
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
Expand All @@ -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
Expand All @@ -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

Expand Down
Loading