Packages

An Ash extension to use prefixed IDs as primary and foreign keys.

Current section

Files

Jump to
Raw

README.md

# AshPrefixedId
An [Ash](https://ash-hq.org/) extension for working with prefixed IDs (e.g. `user_CWzLBdFy2f1XhrtesFferY`).
Inspired by [Stripe's object IDs](https://dev.to/stripe/designing-apis-for-humans-object-ids-3o5a), this library lets you use human-readable, prefixed identifiers while storing standard UUIDs in the database.
## Installation
Add `ash_prefixed_id` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:ash_prefixed_id, "~> 0.2.0"}
]
end
```
## Usage
Add the `AshPrefixedId` extension to your resource and configure a prefix:
```elixir
defmodule App.Blog.Post do
use Ash.Resource,
domain: App.Blog,
data_layer: AshPostgres.DataLayer,
extensions: [AshPrefixedId]
prefixed_id do
prefix "post"
end
actions do
defaults [:read, :destroy, create: [:title], update: [:title]]
end
attributes do
uuid_primary_key :id
attribute :title, :string, public?: true
end
end
```
```elixir
Post
|> Ash.Changeset.for_create(:create, %{title: "Hello world"})
|> Ash.create!()
#=> %Post{id: "post_CWzLBdFy2f1XhrtesFferY"}
```
### Foreign Keys
`belongs_to` relationships automatically get the correct prefixed ID type:
```elixir
relationships do
belongs_to :post, App.Blog.Post
# post_id is auto-created as App.Blog.Post.ObjectId
end
```
### AnyPrefixedId
Register `AnyPrefixedId` as a custom type to accept prefixed IDs anywhere `:uuid` is used:
```elixir
config :ash, custom_types: [uuid: AshPrefixedId.AnyPrefixedId]
```
### PostgreSQL UUIDv7
Enable server-side UUIDv7 generation with `AshPrefixedId.PostgresExtension`:
```elixir
defmodule MyApp.Repo do
use AshPostgres.Repo, otp_app: :my_app
def installed_extensions do
["uuid-ossp", "citext", AshPrefixedId.PostgresExtension]
end
end
```
Then in your resource:
```elixir
prefixed_id do
prefix "post"
migration_default? true
end
```
### Utility Functions
Inside Ash you only ever see prefixed IDs, so you rarely need these. They are
escape hatches for the **boundaries** of your system: validating external input,
or building raw SQL / talking to non-Ash code. Bang variants are the default;
the non-bang `{:error, _}` variants are for external input you expect might be
invalid.
```elixir
# Decode a prefixed ID to the raw 16-byte UUID binary (for SQL fragments)
AshPrefixedId.to_uuid!("user_CWzLBdFy2f1XhrtesFferY")
#=> <<93, 68, 109, 8, ...>>
# Decode to a UUID string
AshPrefixedId.to_uuid_string!("user_CWzLBdFy2f1XhrtesFferY")
#=> "5d446d08-df6a-404d-a1e5-decc78429b3d"
# Non-bang variants for validating EXTERNAL input (e.g. an HTTP param)
AshPrefixedId.to_uuid(param) #=> {:ok, <<...>>} | {:error, :invalid_prefixed_id}
AshPrefixedId.to_uuid_string(param) #=> {:ok, "..."} | {:error, :invalid_prefixed_id}
# Encode a UUID binary back to a prefixed ID (prefix string or resource module)
AshPrefixedId.to_prefixed_id(uuid_binary, "user")
AshPrefixedId.to_prefixed_id(uuid_binary, MyApp.Accounts.User)
# Find which resource a prefixed ID belongs to
AshPrefixedId.find_resource_for_id(domains, "user_CWzLBdFy2f1XhrtesFferY")
# Detect duplicate prefixes across domains
AshPrefixedId.find_duplicate_prefixes(domains)
```
To accept both prefixed IDs and raw UUIDs transparently rather than converting
by hand, register `AshPrefixedId.AnyPrefixedId` as a custom type (see above).
For more detailed information, read the `AshPrefixedId` moduledoc.
## Acknowledgments
This project is a fork of [ash_object_ids](https://github.com/drtheuns/ash_object_ids) by [Randall Theuns](https://github.com/drtheuns). The original work laid the foundation for prefixed ID support in Ash.
## License
MIT — see [LICENSE](LICENSE) for details.