Ithibati.Schema.User (Ithibati v0.1.0)

Copy Markdown View Source

What Ithibati adds to the account schema an application already owns.

defmodule MyApp.Accounts.User do
  use Ecto.Schema
  alias Ithibati.Schema.Identifier
  alias Ithibati.Schema.User

  use User, identifier: :email, format: Identifier.email_format()

  import Ecto.Changeset

  schema "users" do
    ithibati_account()

    field :name, :string
    timestamps(type: :utc_datetime_usec)
  end

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

Options

  • identifier: — required, a literal atom: the field an account is known by. You have to write this one out here, because it is the field your schema declares.
  • format: — optional, a regular expression, evaluated once when your module compiles. Ithibati.Schema.Identifier.email_format/0 offers one for addresses.
  • constraint_name: — optional, the name of the unique index on that column. Say it when your naming convention is not the one Ecto derives. Ithibati then creates the index under that name, and the changeset's constraint matches it.
  • unique_index: false — optional, an opt-out. Say it when your application creates that index itself, and Ithibati checks that one exists rather than creating it.

You may write each of the three value options inline, or name a module attribute standing above the use line. Ithibati refuses an option written as nil, because that is what a misspelled attribute looks like.

identifier_changeset/2 trims and lowercases the values it writes, whatever the field is called.

There is no default, and the macro says so when you leave it out: this library never sends mail, so it will not ask you for an address by assumption. Ithibati.Schema.Identifier.email_format/0 says what the offered email pattern accepts and why it is not RFC 5322.

What it injects, and what it refuses

The macro injects one field, three associations, and three functions: identifier_changeset/2, passkey_display_name/1 (overridable, nil by default) and __ithibati__/1. Ithibati.Schema.UserTest pins that list against a schema that does not use this macro, so a fourth one has to be a decision.

It refuses two things, both at compile time: a module that never calls ithibati_account/0 inside its schema block, and one that defines identifier_changeset/2 or __ithibati__/1 itself.

Summary

Functions

Whether a module carries what this macro injects.

The name and displayName a WebAuthn registration shows, for an account or for an identifier that does not have one yet.

Whether the identifier is what a failed insert collided on.

Declares the identifier field and the associations Ithibati needs. Call it inside your schema block, in a module that has use Ithibati.Schema.User above it.

Functions

account_schema?(module)

Whether a module carries what this macro injects.

Ithibati offers one predicate rather than two spellings of it. The marker function has been renamed once already, and a second caller checking it by hand would be a second thing to find by grep next time.

credential_user(account)

The name and displayName a WebAuthn registration shows, for an account or for an identifier that does not have one yet.

Ithibati derives both here rather than leaving them to the caller, because it owns the fallback: an application's passkey_display_name/1 may answer nil and be right. The first registration on an instance has no account at all, which is why the second clause exists.

identifier_taken?(changeset)

Whether the identifier is what a failed insert collided on.

A transaction that refuses an account hands back an Ecto.Changeset, and the application then has to decide what to tell somebody. "That name is taken" comes from a unique index and "that is not a name" comes from the format. Only Ithibati knows which field is the identifier, because ithibati_account/0 declared it.

The match is on the field, so a uniqueness error on it counts however the index is shaped. An application that scopes the identifier to a tenant gets true for a collision inside that tenant, which is what a collision means there.

The function answers which of the two it was and nothing more. The application turns that into :username_taken, a sentence or an HTTP status, the same way Ithibati.Web.Handler leaves it what a verified assertion is worth.

Only after the database has seen it

A changeset carries a constraint error only after the repo has attempted the insert and been refused. A changeset built and never given to the repo answers false however certainly the identifier is taken, and that looks exactly like this function not working.

ithibati_account()

(macro)

Declares the identifier field and the associations Ithibati needs. Call it inside your schema block, in a module that has use Ithibati.Schema.User above it.