Skip to content

An Elixir implementation of CUID2 (Collision-Resistant Unique Identifiers).

License

Notifications You must be signed in to change notification settings

juliolinarez/ex_cuid2

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

9 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ExCuid2

Hex.pm Hex Docs

ExCuid2 is a robust, thread-safe, and testable CUID2 (Collision-Resistant Unique Identifiers) generator for Elixir.

This implementation follows the CUID2 standard and generates safe, horizontally scalable IDs, ideal for use as primary keys in databases.

Features:

  • Prefix with a random letter.
  • Timestamp in milliseconds.
  • Local Atomic counter to prevent collisions in the same millisecond.
  • Cryptographically secure entropy.
  • Process fingerprint to ensure uniqueness across nodes.

Installation

The package is available in Hex and can be installed by adding ex_cuid2 to your list of dependencies in mix.exs:

def deps do
  [
    {:ex_cuid2, "~> 0.10.1"}
  ]
end

Usage

Generating IDs

You can generate a CUID2 with the default length (24) or specify a custom length.

# Generate a default CUID2
iex> ExCuid2.generate()
"v8p7k3f9z1m0c2x4b6n5j7h8"

# Generate a CUID2 with a custom length (e.g., 30)
iex> ExCuid2.generate(30)
"b5n6m4j3h2g1f0d9s8a7q6w5e4r3t2"

Validating a CUID2

You can check if a given string conforms to the CUID2 format using is_valid?/1. It performs a check based on length and character set and returns false for any non-binary input without raising an error.

iex> id = ExCuid2.generate()
"t9p7k3f9z1m0c2x4b6n5j7h8"

iex> ExCuid2.is_valid?(id)
true

# --- Invalid Cases ---

# Too short
iex> ExCuid2.is_valid?("a123")
false

# Starts with a number
iex> ExCuid2.is_valid?("1abcdefghijklmnopqrstuvw")
false

# Contains invalid characters (uppercase)
iex> ExCuid2.is_valid?("aBcdefghijklmnopqrstuvwX")
false

# Wrong data type
iex> ExCuid2.is_valid?(12345)
false

Ecto Integration

ExCuid2 provides an optional Ecto.Type module for seamless integration with your Ecto schemas.

1. Configure Your Repo

First, register ExCuid2.Ecto.Type as a custom type in your application's configuration (config/config.exs).

# in config/config.exs
config :my_app, MyApp.Repo,
  ecto_types: [cuid2: ExCuid2.Ecto.Type]

(Replace :my_app and MyApp.Repo with your application's name and Repo module.)

2. Use in Your Schema

It's recommended to use it for your primary key with autogenerate: true and to set the @foreign_key_type.

defmodule MyApp.Accounts.User do
  use Ecto.Schema
  import Ecto.Changeset

  # Define the primary key as a CUID2
  @primary_key {:id, ExCuid2.Ecto.Type, autogenerate: true}
  @foreign_key_type ExCuid2.Ecto.Type
  schema "users" do
    field :name, :string
    field :email, :string

    timestamps()
  end

  def changeset(user, attrs) do
    user
    |> cast(attrs, [:name, :email])
    |> validate_required([:name, :email])
  end
end

You could use this : @primary_key {:id, ExCuid2.Ecto.Type, autogenerate: 32} if you use a cuid2 longer.

With this setup, Ecto will automatically generate a new CUID2 for the :id field whenever you insert a new record, giving you secure and scalable primary keys out of the box.

defmodule MyApp.Accounts.Migrations.CreateAccount do
  use Ecto.Migration

  def change do
    # primary_key: false is not needed here since we are defining a custom primary key
    create table(:record, primary_key: false) do
      # we use char beacuse cuid2 has a fixed length.
      add :id, :char, primary_key: true, size: 24 # check the cuid2 length
      add :title, :string
      add :body, :string

      timestamps(type: :utc_datetime)
    end
    
    # Use a hash index for random values as cuid2.
    create index(:record, [:id], using: :hash)
  end
end

Optional Custom Postgres Domain

  defmodule MyApp.Repo.Migrations.CreateCuid2DomainType do
    use Ecto.Migration
    # Create a custom domain type for cuid2
    # This is a PostgreSQL specific feature, so ensure your database supports it.
    # The cuid2 format is a 24-32 character string starting with a letter followed by lowercase alphanumeric characters.
    # The regex checks that the value starts with a letter and is followed by 23 lowercase alphanumeric characters.
    # Adjust the regex as necessary to fit your specific requirements.
    # Note: The size of 24 is based on the standard CUID2 length.
    # If you are using a different length, adjust the size accordingly.
    def up do
      execute("""
      CREATE DOMAIN cuid2 AS character(24)
        CONSTRAINT cuid2_check CHECK (VALUE ~ '^[a-z][a-z0-9]{23}$');
      """)
    end

    def down do
      execute("DROP DOMAIN cuid2;")
    end
  end

After this migration you can use this :

defmodule MyApp.Repo.Migrations.CreateRecord do
  use Ecto.Migration

  def change do
    create table(:record, primary_key: false) do
      # add :id, :char, primary_key: true, size: 24
      add :id, :cuid2, primary_key: true
      add :title, :string
      add :body, :string

      timestamps(type: :utc_datetime)
    end

    # Use a hash index for random values as cuid2.
    create index(:record, [:id], using: :hash)
  end
end

About

An Elixir implementation of CUID2 (Collision-Resistant Unique Identifiers).

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages