Use cid
for universally unique ids
By default all the tables
in a Phoenix
Application
use an auto-incrementing integer
(Serial
)
for the id
(Primary Key).
e.g the items
table:
This is fine for a server-side rendered app
with a single relational database instance.
i.e. the server/database controls the id
of records.
But our ambition has always been
to build a mobile + offline-first distributed App.
Luckily our friends at
Protocol Labs
creators of
IPFS
have done some great groundwork on
"Decentralized Apps"
so we can build on that.
We created an Elixir
package
that creates IPFS
compliant
Content Identifiers:
cid
The basic usage is:
item = %{text: "Build PARA System App", person_id: 2, status: 2}
Cid.cid(item)
"zb2rhn92tqTt41uFZ3hh3VPnssXjYCW4yDSX7KB39dXZyMtNC"
This cid
string is unique to this content
therefore creating it on the client (Mobile device)
will generate the same cid
if the record is created offline.
We can easily confirm the validity of this cid
by inputting it into CID Inspector:
cid.ipfs.tech
e.g:
https://cid.ipfs.tech/#zb2rhn92tqTt41uFZ3hh3VPnssXjYCW4yDSX7KB39dXZyMtNC
Add excid
package to deps
Add the excid
package to the deps
in mix.exs
:
# Universally Unique Deterministic Content IDs: github.com/dwyl/cid
{:excid, "~> 1.0.1"},
Run:
mix deps.get
Then in config/config.exs
add the following configuration line:
config :excid, base: :base58
We want to use
base58
because it uses 17% fewer characters to represent the same ID
when compared to base32
.
"bafkreihght5nbnmn6xbwoakmjyhkiu2naxmwpxgxbp6xkpbiweuetbohde"
|> String.length()
59
vs.
"zb2rhn92tqTt41uFZ3hh3VPnssXjYCW4yDSX7KB39dXZyMtNC"
|> String.length()
49
Create migration
Using the
mix ecto.gen.migration
command,
create a migration file:
mix ecto.gen.migration add_cid
You should see output similar to the following:
* creating priv/repo/migrations/20230824153220_add_cid.exs
This tells us that the migration file was created:
priv/repo/migrations/20230824153220_add_cid_to_item.exs
Open the file, you should see a blank migration:
defmodule App.Repo.Migrations.AddCidToItem do
use Ecto.Migration
def change do
end
end
Update it to:
defmodule App.Repo.Migrations.AddCidToItem do
use Ecto.Migration
def change do
alter table(:items) do
add(:cid, :string)
end
end
end
Note: if you're rusty on
migrations
, see: devhints.io/phoenix-migrations
Add cid
field to item
schema
Open the
lib/app/item.ex
file and locate the schema items do
section.
Add the line:
field :cid, :string
Create put_cid/1
test
Create a new file with the path:
test/app/cid_test.exs
and add the following test:
defmodule App.CidTest do
use App.DataCase, async: true
@valid_attrs %{text: "Buy Bananas", person_id: 1, status: 2}
test "put_cid/1 adds a `cid` for the `item` record" do
# Create a changeset with a valid item record as the "changes":
changeset_before = %{changes: @valid_attrs}
# Should not yet have a cid:
refute Map.has_key?(changeset_before.changes, :cid)
# Confirm cid was added to the changes:
changeset_with_cid = App.Cid.put_cid(changeset_before)
assert changeset_with_cid.changes.cid == Cid.cid(@valid_attrs)
# confirm idempotent:
assert App.Cid.put_cid(changeset_with_cid) == changeset_with_cid
end
end
Running this test will fail:
mix test test/app/cid_test.exs
E.g:
** (UndefinedFunctionError) function App.Cid.put_cid/1 is undefined or private.
Let's implement it!
Create put_cid/1
function
Create a new file with the path:
lib/app/cid.ex
.
Add the following function definition to the file:
defmodule App.Cid do
def put_cid(changeset) do
if Map.has_key?(changeset.changes, :cid) do
changeset
else
cid = Cid.cid(changeset.changes)
%{changeset | changes: Map.put(changeset.changes, :cid, cid)}
end
end
end
The function just adds a cid
to the changeset.changes
so that it creates a hash of the contents of the changeset
and then adds the cid
to identify that content.
If the changes
already have a cid
don't do anything.
This covers the case where an item
is created in the Mobile client
with a cid
. We will add verification for this later.
Invoke put_cid/1
in Item.changeset/2
In the
lib/app/item.ex
file, locate the changeset/2
function definition
and change the lines:
|> cast(attrs, [:person_id, :status, :text])
|> validate_required([:text, :person_id])
To:
|> cast(attrs, [:cid, :person_id, :status, :text])
|> validate_required([:text, :person_id])
|> App.Cid.put_cid()
The call to put_cid/1
within changeset/2
adds the cid
to the item
record.
Checkpoint: item
table with cid
After running the migration:
mix ecto.migrate
The item
table has the cid
column:
Even though the cid
field was added to the table,
it is empty for all existing item
records.
Easily resolved.
Update all item
records
Add the following function to
lib/app/item.ex
:
def update_all_items_cid do
items = list_items()
Enum.each(items, fn i ->
item = %{
person_id: i.person_id,
status: i.status,
text: i.text,
id: i.id,
}
i
|> changeset(Map.put(item, :cid, Cid.cid(item)))
|> Repo.update()
end)
end
end
Invoke it in the
priv/repo/seeds.exs
so that all items
are given a cid
.
With that working we can get back to using this
in our next feature; lists
!