AshPrefixedId (ash_prefixed_id v0.2.0)
View SourceAn extension for working with prefixed IDs.
Prefixed IDs are identifiers that are prefixed with the resource they identify. A more detailed explanation can be found in the "Designing APIs for humans" blog post.
This library provides an implementation for Ash:
defmodule App.Blog.Post do
use Ash.Resource,
domain: App.Blog,
data_layer: Ash.DataLayer.AshPostgres,
extensions: [AshPrefixedId]
prefixed_id do
prefix "p"
end
attributes do
uuid_primary_key(:id)
# ... other attributes
end
endAshPrefixedId replaces the :id primary key with a prefixed ID (prefixed by "p").
The underlying UUID implementation will be used, so it works with both UUID
and UUIDv7. The IDs are stored as regular UUIDs in the database. Externally,
the UUIDs are encoded as "{prefix}_{base58(uuid)}".
Each resource will have a generated <resource>.ObjectId module which is the
Ash.Type for that ID. Foreign key attributes for belongs_to relationships
are automatically created with the correct ObjectId type:
relationships do
belongs_to :post, App.Blog.Post
# post_id attribute is auto-created as App.Blog.Post.ObjectId
endWorking with IDs: the Ash boundary
Inside Ash you only ever deal with prefixed IDs — the raw UUID never
surfaces. Cast inputs, action results, and belongs_to foreign keys are all
prefixed. You therefore rarely need the conversion helpers in this module;
they exist for the boundaries of your system:
- Validating external input (HTTP params, request bodies, file contents)
before it enters Ash — use the non-bang
to_uuid/1/to_uuid_string/1and handle{:error, :invalid_prefixed_id}. - Building raw SQL fragments, or talking to non-Ash code that needs the raw
UUID — use the bang
to_uuid!/1/to_uuid_string!/1. IDs that come from inside Ash are always valid, so let it crash.
Bang is the default inside Ash; reach for the non-bang variants only when an invalid value is genuinely expected (i.e. at an external boundary).
If you find yourself sniffing the ID format (e.g. String.starts_with?(id, "user_")) or handling "both raw UUID and prefixed" forms, that is a sign a
raw UUID has leaked across a boundary — fix the caller, don't normalize
everywhere. To accept both prefixed IDs and raw UUIDs transparently for a
:uuid field, register AshPrefixedId.AnyPrefixedId as a custom type instead
of converting by hand.
Summary
Functions
Same as map_prefixes_to_resources, but returns only the entries that
contain more than resource for the given prefix.
Same as find_resource_for_prefix/2 but accepts a (valid) prefixed ID.
Searches the given domains for the resource that matches the given prefixed ID prefix.
Create a map of prefixes to the resources that use that prefix.
Encode a raw 16-byte UUID binary as a prefixed ID.
Decodes a prefixed ID into the raw 16-byte UUID binary, returning a tagged tuple.
Like to_uuid/1 but returns the raw 16-byte UUID binary directly, raising on
invalid input. This is the default inside Ash, where IDs are always valid.
Decodes a prefixed ID into a UUID string, returning a tagged tuple.
Like to_uuid_string/1 but returns the UUID string directly, raising on
invalid input. This is the default inside Ash.
Functions
Same as map_prefixes_to_resources, but returns only the entries that
contain more than resource for the given prefix.
This function can be used to warn whenever duplicate prefixes are present in your modules.
Same as find_resource_for_prefix/2 but accepts a (valid) prefixed ID.
Examples
iex> find_resource_for_id(domains, "user_CWzLBdFy2f1XhrtesFferY")
MyApp.Accounts.User
iex> find_resource_for_id(domains, "florb_CWzLBdFy2f1XhrtesFferY")
nil
Searches the given domains for the resource that matches the given prefixed ID prefix.
You can get the domains through Application.get_env(:my_otp_app, :ash_domains, [])
Examples
iex> find_resource_for_prefix(domains, "user")
MyApp.Accounts.User
iex> find_resource_for_prefix(domains, "florb")
nil
Create a map of prefixes to the resources that use that prefix.
Encode a raw 16-byte UUID binary as a prefixed ID.
The second argument is either a prefix string or a resource module. Passing
the resource module resolves the prefix from its prefixed_id DSL, which
avoids hard-coding (and drifting from) the configured prefix.
Encoding is infallible, so there is no bang variant. This is an escape hatch for raw SQL / non-Ash code; inside Ash you receive prefixed IDs directly.
Examples
iex> to_prefixed_id(<<93, 68, 109, 8, ...>>, "user")
"user_CWzLBdFy2f1XhrtesFferY"
iex> to_prefixed_id(<<93, 68, 109, 8, ...>>, MyApp.Accounts.User)
"user_CWzLBdFy2f1XhrtesFferY"
Decodes a prefixed ID into the raw 16-byte UUID binary, returning a tagged tuple.
This is the non-bang variant: reach for it only when validating external
input (HTTP params, request bodies, file contents) at a boundary, where a bad
value is expected and you want to handle it gracefully. Inside Ash you only
ever see valid prefixed IDs, so prefer to_uuid!/1 there.
Examples
iex> to_uuid("user_CWzLBdFy2f1XhrtesFferY")
{:ok, <<93, 68, 109, 8, ...>>}
iex> to_uuid("not a prefixed id")
{:error, :invalid_prefixed_id}
Like to_uuid/1 but returns the raw 16-byte UUID binary directly, raising on
invalid input. This is the default inside Ash, where IDs are always valid.
Examples
iex> to_uuid!("user_CWzLBdFy2f1XhrtesFferY")
<<93, 68, 109, 8, ...>> # 16-byte binary
Decodes a prefixed ID into a UUID string, returning a tagged tuple.
The non-bang variant, for validating external input at a boundary. Inside
Ash, prefer to_uuid_string!/1.
Examples
iex> to_uuid_string("user_CWzLBdFy2f1XhrtesFferY")
{:ok, "5d446d08-df6a-404d-a1e5-decc78429b3d"}
iex> to_uuid_string("not a prefixed id")
{:error, :invalid_prefixed_id}
Like to_uuid_string/1 but returns the UUID string directly, raising on
invalid input. This is the default inside Ash.
Examples
iex> to_uuid_string!("user_CWzLBdFy2f1XhrtesFferY")
"5d446d08-df6a-404d-a1e5-decc78429b3d"