Packages
phoenix_kit
1.7.36
1.7.208
1.7.207
1.7.206
1.7.205
1.7.204
1.7.203
1.7.202
1.7.201
1.7.200
1.7.199
1.7.198
1.7.197
1.7.196
1.7.194
1.7.193
1.7.192
1.7.191
1.7.190
1.7.189
1.7.187
1.7.186
1.7.185
1.7.184
1.7.183
1.7.182
1.7.181
1.7.180
1.7.179
1.7.178
1.7.177
1.7.176
1.7.175
1.7.174
1.7.173
1.7.172
1.7.171
1.7.170
1.7.169
1.7.168
1.7.167
1.7.166
1.7.165
1.7.164
1.7.162
1.7.161
1.7.160
1.7.159
1.7.157
1.7.156
1.7.155
1.7.154
1.7.153
1.7.152
1.7.151
1.7.150
1.7.149
1.7.146
1.7.145
1.7.144
1.7.143
1.7.138
1.7.133
1.7.132
1.7.131
1.7.130
1.7.128
1.7.126
1.7.125
1.7.121
1.7.120
1.7.119
1.7.118
1.7.117
1.7.116
1.7.115
1.7.114
1.7.113
1.7.112
1.7.111
1.7.110
1.7.109
1.7.108
1.7.107
1.7.106
1.7.105
1.7.104
1.7.103
1.7.102
1.7.101
1.7.100
1.7.99
1.7.98
1.7.97
1.7.96
1.7.95
1.7.94
1.7.93
1.7.92
1.7.91
1.7.90
1.7.89
1.7.88
1.7.87
1.7.86
1.7.85
1.7.84
1.7.83
1.7.82
1.7.81
1.7.80
1.7.79
1.7.78
1.7.77
1.7.76
1.7.75
1.7.74
1.7.71
1.7.70
1.7.69
1.7.66
1.7.65
1.7.64
1.7.63
1.7.62
1.7.61
1.7.59
1.7.58
1.7.57
1.7.56
1.7.55
1.7.54
1.7.53
1.7.52
1.7.51
1.7.49
1.7.44
1.7.43
1.7.42
1.7.41
1.7.39
1.7.38
1.7.37
1.7.36
1.7.34
1.7.33
1.7.31
1.7.30
1.7.29
1.7.28
1.7.27
1.7.26
1.7.25
1.7.24
1.7.23
1.7.22
1.7.21
1.7.20
1.7.19
1.7.18
1.7.17
1.7.16
1.7.15
1.7.14
1.7.13
1.7.12
1.7.11
1.7.10
1.7.9
1.7.8
1.7.7
1.7.6
1.7.5
1.7.4
1.7.3
1.7.2
1.7.1
1.7.0
1.6.20
1.6.19
1.6.18
1.6.17
1.6.16
1.6.15
1.6.14
1.6.13
1.6.12
1.6.11
1.6.10
1.6.9
1.6.8
1.6.7
1.6.6
1.6.5
1.6.4
1.6.3
1.5.2
1.5.1
1.5.0
1.4.9
1.4.8
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.2
1.3.1
1.3.0
1.2.10
1.2.9
1.2.8
1.2.7
1.2.5
1.2.4
1.2.2
1.2.1
1.2.0
1.1.0
1.0.0
A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more
Current section
Files
Jump to
Current section
Files
lib/phoenix_kit/uuid.ex
defmodule PhoenixKit.UUID do
@moduledoc """
TEMPORARY: Dual ID lookup utilities for UUID migration transition.
> #### Transitional Module {: .warning}
>
> This module exists ONLY for the transition period while PhoenixKit
> migrates from bigserial to UUID primary keys. It will be **removed
> in PhoenixKit 2.0** when all tables use UUID as the primary key.
>
> - For UUID generation, use `UUIDv7.generate()` directly
> - For lookups, use standard `Repo.get/2` with UUIDs after 2.0
>
> **DELETE THIS MODULE** when PhoenixKit switches to UUID-native PKs.
## Purpose
This module provides helper functions for working with dual ID systems
(bigserial + UUID column) during the transition period. It enables parent
applications to accept either integer IDs or UUIDs in URLs/APIs.
## When to Use
Use this module in parent application controllers/LiveViews when you want
to accept either ID type in user-facing URLs:
# In your controller - accepts /users/5 OR /users/019b57...
def show(conn, %{"id" => identifier}) do
user = PhoenixKit.UUID.get(User, identifier)
end
## When NOT to Use
- PhoenixKit internal code still uses integer IDs for all operations
- Foreign key relationships remain integer-based
- If you know you have an integer ID, use `Repo.get/2` directly
## Usage Examples
# Dual lookup (auto-detects type)
PhoenixKit.UUID.get(User, "123") # integer lookup
PhoenixKit.UUID.get(User, "019b57...") # UUID lookup
# With multi-tenant prefix
PhoenixKit.UUID.get(User, id, prefix: "tenant_123")
# UUID generation (prefer UUIDv7.generate() directly)
PhoenixKit.UUID.generate()
## Migration Timeline
1. **V40 (current)**: UUID columns added, this helper available
2. **Transition**: Parent apps can start using UUIDs in URLs
3. **2.0**: UUID becomes PK, this module is deleted
"""
alias PhoenixKit.RepoHelper
@uuid_regex ~r/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
@doc """
Generates a new UUIDv7 string.
UUIDv7 is a time-ordered UUID where the first 48 bits are a Unix timestamp
in milliseconds, providing natural chronological ordering.
## Examples
iex> PhoenixKit.UUID.generate()
"019b5704-3680-7b95-9d82-ef16127f1fd2"
"""
@spec generate() :: String.t()
def generate do
UUIDv7.generate()
end
@doc """
Gets a record by either integer ID or UUID string.
Automatically detects the identifier type and performs the appropriate lookup.
Returns `nil` if the record is not found.
## Parameters
- `schema` - The Ecto schema module
- `identifier` - Either an integer ID, string integer, or UUID string
- `opts` - Optional keyword list:
- `:prefix` - Schema prefix for multi-tenant databases
- `:repo` - Override the repository to use
## Examples
iex> PhoenixKit.UUID.get(User, 123)
%User{id: 123, uuid: "..."}
iex> PhoenixKit.UUID.get(User, "123")
%User{id: 123, uuid: "..."}
iex> PhoenixKit.UUID.get(User, "019b5704-3680-7b95-9d82-ef16127f1fd2")
%User{id: 123, uuid: "019b5704-3680-7b95-9d82-ef16127f1fd2"}
iex> PhoenixKit.UUID.get(User, "nonexistent")
nil
# With prefix for multi-tenant schemas
iex> PhoenixKit.UUID.get(User, "123", prefix: "tenant_abc")
%User{id: 123, uuid: "..."}
"""
@spec get(module(), integer() | String.t(), keyword()) :: struct() | nil
def get(schema, identifier, opts \\ []) do
case parse_identifier(identifier) do
{:integer, id} -> get_by_id(schema, id, opts)
{:uuid, uuid} -> get_by_uuid(schema, uuid, opts)
:invalid -> nil
end
end
@doc """
Gets a record by either integer ID or UUID string, raising if not found.
## Examples
iex> PhoenixKit.UUID.get!(User, "019b5704-3680-7b95-9d82-ef16127f1fd2")
%User{uuid: "019b5704-3680-7b95-9d82-ef16127f1fd2"}
iex> PhoenixKit.UUID.get!(User, "nonexistent")
** (Ecto.NoResultsError)
"""
@spec get!(module(), integer() | String.t(), keyword()) :: struct()
def get!(schema, identifier, opts \\ []) do
case get(schema, identifier, opts) do
nil -> raise Ecto.NoResultsError, queryable: schema
record -> record
end
end
@doc """
Gets a record by its integer ID.
## Options
- `:prefix` - Schema prefix for multi-tenant databases
- `:repo` - Override the repository to use
## Examples
iex> PhoenixKit.UUID.get_by_id(User, 123)
%User{id: 123}
iex> PhoenixKit.UUID.get_by_id(User, 999999)
nil
iex> PhoenixKit.UUID.get_by_id(User, 123, prefix: "tenant_abc")
%User{id: 123}
"""
@spec get_by_id(module(), integer() | String.t(), keyword()) :: struct() | nil
def get_by_id(schema, id, opts \\ [])
def get_by_id(schema, id, opts) when is_integer(id) do
repo = Keyword.get(opts, :repo, RepoHelper.repo())
query_opts = build_query_opts(opts)
repo.get_by(schema, [id: id], query_opts)
end
def get_by_id(schema, id, opts) when is_binary(id) do
case Integer.parse(id) do
{int_id, ""} -> get_by_id(schema, int_id, opts)
_ -> nil
end
end
@doc """
Gets a record by its UUID.
## Options
- `:prefix` - Schema prefix for multi-tenant databases
- `:repo` - Override the repository to use
## Examples
iex> PhoenixKit.UUID.get_by_uuid(User, "019b5704-3680-7b95-9d82-ef16127f1fd2")
%User{uuid: "019b5704-3680-7b95-9d82-ef16127f1fd2"}
iex> PhoenixKit.UUID.get_by_uuid(User, "nonexistent-uuid")
nil
iex> PhoenixKit.UUID.get_by_uuid(User, "019b5704-...", prefix: "tenant_abc")
%User{uuid: "019b5704-..."}
"""
@spec get_by_uuid(module(), String.t(), keyword()) :: struct() | nil
def get_by_uuid(schema, uuid, opts \\ [])
def get_by_uuid(schema, uuid, opts) when is_binary(uuid) do
if valid_uuid?(uuid) do
repo = Keyword.get(opts, :repo, RepoHelper.repo())
query_opts = build_query_opts(opts)
repo.get(schema, uuid, query_opts)
else
nil
end
end
@doc """
Parses an identifier and returns its type.
## Examples
iex> PhoenixKit.UUID.parse_identifier(123)
{:integer, 123}
iex> PhoenixKit.UUID.parse_identifier("123")
{:integer, 123}
iex> PhoenixKit.UUID.parse_identifier("019b5704-3680-7b95-9d82-ef16127f1fd2")
{:uuid, "019b5704-3680-7b95-9d82-ef16127f1fd2"}
iex> PhoenixKit.UUID.parse_identifier("invalid")
:invalid
"""
@spec parse_identifier(integer() | String.t()) ::
{:integer, integer()} | {:uuid, String.t()} | :invalid
def parse_identifier(id) when is_integer(id), do: {:integer, id}
def parse_identifier(id) when is_binary(id) do
cond do
valid_uuid?(id) ->
{:uuid, id}
integer_string?(id) ->
{int_id, ""} = Integer.parse(id)
{:integer, int_id}
true ->
:invalid
end
end
def parse_identifier(_), do: :invalid
@doc """
Checks if a string is a valid UUID format.
Works with both UUIDv4 and UUIDv7 formats.
## Examples
iex> PhoenixKit.UUID.valid_uuid?("019b5704-3680-7b95-9d82-ef16127f1fd2")
true
iex> PhoenixKit.UUID.valid_uuid?("not-a-uuid")
false
"""
@spec valid_uuid?(String.t()) :: boolean()
def valid_uuid?(string) when is_binary(string) do
Regex.match?(@uuid_regex, string)
end
def valid_uuid?(_), do: false
@doc """
Checks if an identifier is a UUID (vs integer).
## Examples
iex> PhoenixKit.UUID.uuid?("019b5704-3680-7b95-9d82-ef16127f1fd2")
true
iex> PhoenixKit.UUID.uuid?("123")
false
iex> PhoenixKit.UUID.uuid?(123)
false
"""
@spec uuid?(any()) :: boolean()
def uuid?(identifier) do
case parse_identifier(identifier) do
{:uuid, _} -> true
_ -> false
end
end
@doc """
Checks if an identifier is an integer ID (vs UUID).
## Examples
iex> PhoenixKit.UUID.integer_id?(123)
true
iex> PhoenixKit.UUID.integer_id?("123")
true
iex> PhoenixKit.UUID.integer_id?("019b5704-3680-7b95-9d82-ef16127f1fd2")
false
"""
@spec integer_id?(any()) :: boolean()
def integer_id?(identifier) do
case parse_identifier(identifier) do
{:integer, _} -> true
_ -> false
end
end
@doc """
Extracts the UUID from a record, returning nil if not present.
## Examples
iex> PhoenixKit.UUID.extract_uuid(%User{uuid: "019b5704-..."})
"019b5704-..."
iex> PhoenixKit.UUID.extract_uuid(%User{uuid: nil})
nil
"""
@spec extract_uuid(struct()) :: String.t() | nil
def extract_uuid(%{uuid: uuid}), do: uuid
def extract_uuid(_), do: nil
@doc """
Extracts the integer ID from a record.
## Examples
iex> PhoenixKit.UUID.extract_id(%User{id: 123})
123
"""
@spec extract_id(struct()) :: integer() | nil
def extract_id(%{id: id}), do: id
def extract_id(_), do: nil
@doc """
Returns the preferred identifier for a record.
Prefers UUID if available, falls back to integer ID.
## Examples
iex> PhoenixKit.UUID.preferred_identifier(%User{id: 123, uuid: "019b5704-..."})
"019b5704-..."
iex> PhoenixKit.UUID.preferred_identifier(%User{id: 123, uuid: nil})
123
"""
@spec preferred_identifier(struct()) :: String.t() | integer() | nil
def preferred_identifier(%{uuid: uuid}) when is_binary(uuid), do: uuid
def preferred_identifier(%{id: id}), do: id
def preferred_identifier(_), do: nil
# Private helpers
defp integer_string?(string) do
case Integer.parse(string) do
{_, ""} -> true
_ -> false
end
end
# Build query options from keyword list, filtering to Ecto-supported options
defp build_query_opts(opts) do
opts
|> Keyword.take([:prefix])
|> Enum.reject(fn {_k, v} -> is_nil(v) end)
end
end