Global IDs
View SourceGreenFairy provides a flexible Global ID system for Relay Object Identification. Global IDs uniquely identify objects across your entire schema, encoding both the type and local ID.
Overview
Global IDs are used for:
- The Relay
node(id: ID!)query - The
expose :idtype macro - Any field requiring globally unique identification
Default Implementation
By default, GreenFairy uses Base64 encoding following the Relay specification:
# Format: Base64("TypeName:localId")
GreenFairy.GlobalId.encode("User", 123)
#=> "VXNlcjoxMjM="
GreenFairy.GlobalId.decode("VXNlcjoxMjM=")
#=> {:ok, {"User", "123"}}Custom GlobalId Implementation
Implement the GreenFairy.GlobalId behaviour to use custom encoding:
defmodule MyApp.HashidsGlobalId do
@behaviour GreenFairy.GlobalId
@impl true
def encode(type_name, id) when is_atom(type_name) do
encode(atom_to_type_name(type_name), id)
end
def encode(type_name, id) when is_binary(type_name) do
# Use Hashids for shorter, URL-safe IDs
Hashids.encode(hashids(), [type_index(type_name), id])
end
@impl true
def decode(global_id) do
case Hashids.decode(hashids(), global_id) do
{:ok, [type_index, local_id]} ->
{:ok, {index_to_type(type_index), local_id}}
_ ->
{:error, :invalid_global_id}
end
end
defp hashids do
Hashids.new(salt: "my-secret-salt", min_len: 8)
end
defp type_index("User"), do: 1
defp type_index("Post"), do: 2
defp type_index("Comment"), do: 3
# ...
defp index_to_type(1), do: "User"
defp index_to_type(2), do: "Post"
defp index_to_type(3), do: "Comment"
# ...
defp atom_to_type_name(atom) do
atom |> Atom.to_string() |> Macro.camelize()
end
endConfiguring GlobalId
Per-Schema Configuration
defmodule MyApp.GraphQL.Schema do
use GreenFairy.Schema,
query: MyApp.GraphQL.RootQuery,
repo: MyApp.Repo,
global_id: MyApp.HashidsGlobalId
endApplication-Wide Configuration
# config/config.exs
config :green_fairy,
global_id: MyApp.HashidsGlobalIdAPI Reference
GreenFairy.GlobalId Behaviour
@callback encode(type_name :: atom() | String.t(), id :: any()) :: String.t()
@callback decode(global_id :: String.t()) :: {:ok, {String.t(), any()}} | {:error, term()}Module Functions
| Function | Description |
|---|---|
encode(type, id) | Encodes using configured implementation |
decode(global_id) | Decodes, returns {:ok, {type, id}} |
decode!(global_id) | Decodes, raises on error |
decode_id(global_id) | Decodes and parses integer IDs |
type(global_id) | Extracts just the type name |
local_id(global_id) | Extracts just the local ID |
default() | Returns the configured implementation |
Usage Examples
alias GreenFairy.GlobalId
# Encoding
GlobalId.encode("User", 123)
#=> "VXNlcjoxMjM=" (or custom encoding)
GlobalId.encode(:user_profile, 42)
#=> "VXNlclByb2ZpbGU6NDI=" (atoms converted to PascalCase)
# Decoding
GlobalId.decode("VXNlcjoxMjM=")
#=> {:ok, {"User", "123"}}
GlobalId.decode!("VXNlcjoxMjM=")
#=> {"User", "123"}
# Parse integer IDs
GlobalId.decode_id("VXNlcjoxMjM=")
#=> {:ok, {"User", 123}} # Note: integer, not string
# Extract parts
GlobalId.type("VXNlcjoxMjM=")
#=> {:ok, "User"}
GlobalId.local_id("VXNlcjoxMjM=")
#=> {:ok, "123"}Integration Points
Node Resolution
The node_field macro uses GlobalId to resolve nodes:
queries do
node_field() # Uses configured GlobalId for decoding
endType Expose
The expose :id macro uses GlobalId for ID decoding:
type "User", struct: MyApp.User do
expose :id # Decodes GlobalId before fetching
endManual Resolution
Use GlobalId in custom resolvers:
field :transfer_ownership, :item do
arg :item_id, non_null(:id)
arg :new_owner_id, non_null(:id)
resolve fn _, args, ctx ->
with {:ok, {"Item", item_id}} <- GlobalId.decode_id(args.item_id),
{:ok, {"User", user_id}} <- GlobalId.decode_id(args.new_owner_id) do
MyApp.Items.transfer(item_id, user_id)
end
end
endBest Practices
- Keep encoding stable - Changing encoding breaks existing client IDs
- Use integer parsing - Use
decode_id/1when you need integer IDs - Handle errors gracefully - Always pattern match decode results
- Test with edge cases - Test with special characters, long IDs, etc.
Backwards Compatibility
GreenFairy.Relay.GlobalId delegates to the new system for backwards compatibility:
# Both work identically
GreenFairy.Relay.GlobalId.encode("User", 123)
GreenFairy.GlobalId.encode("User", 123)