# Cheatsheet

## Defining schemas
{: .col-2}

### Schema

```elixir
defmodule Person do
  use TypedEctoSchema

  typed_schema "people" do
    field(:name, :string, enforce: true, null: false)
    field(:age, :integer) :: non_neg_integer() | nil
    belongs_to(:company, Company)
    timestamps()
  end
end
```

Generates the schema, `@enforce_keys [:name]` and `@type t()`.

### Embedded schema

```elixir
defmodule Address do
  use TypedEctoSchema

  typed_embedded_schema do
    field(:street, :string)
    field(:city, :string, null: false)
  end
end
```

`typed_embedded_schema` replaces `embedded_schema`; everything else works the same.

## Type inference
{: .col-2}

### Fields

```elixir
field(:name, :string)
# String.t() | nil

field(:count, :integer)
# integer() | nil

field(:tags, {:array, :string})
# list(String.t()) | nil

field(:role, Ecto.Enum, values: [:admin, :user])
# (:admin | :user) | nil

field(:price, Money)
# Money.t() | nil — custom types assume a t/0
```

### Associations and embeds

```elixir
has_one(:profile, Profile)
# Ecto.Schema.has_one(Profile.t()) | nil

has_many(:posts, Post)
# Ecto.Schema.has_many(Post.t())

belongs_to(:company, Company)
# company: Ecto.Schema.belongs_to(Company.t()) | nil
# company_id: integer() | nil

embeds_one(:address, Address)
# Address.t() | nil

embeds_many(:entries, Entry)
# list(Entry.t())
```

"Many" fields are always lists — never `| nil`.

## Field options
{: .col-2}

### `:null` — control `| nil`

```elixir
field(:name, :string, null: false)
# name: String.t()

field(:phone, :string)
# phone: String.t() | nil (default)
```

Never applies to `has_many`/`embeds_many` (always lists); `has_one`/`belongs_to` associations always get `| nil`.

### `:enforce` — add to `@enforce_keys`

```elixir
field(:name, :string, enforce: true)
# @enforce_keys [:name]
```

### `:doc` — document the field

```elixir
field(:name, :string, doc: "The person's full name")
```

Rendered through the moduledoc marker and the generated `@typedoc` (see below).

### `::` — override the typespec

```elixir
field(:age, :integer) :: non_neg_integer() | nil
# age: non_neg_integer() | nil (verbatim)
```

The override always wins over inference and is used exactly as written — add `| nil` yourself if wanted.

## Schema-level options
{: .col-2}

### Defaults for all fields

```elixir
typed_schema "people", null: false, enforce: true do
  # String.t(), enforced
  field(:name, :string)
  # opt back out per field
  field(:age, :integer, null: true)
  # defaults are not enforced
  field(:bio, :string, default: "")
end
```

### Other options

```elixir
typed_schema "people", opaque: true do
  # @opaque t()
end

typed_schema "people", additional_types: true do
  # named types for Ecto.Enum fields (experimental)
end
```

## Generated fields
{: .col-2}

### `@primary_key`

```elixir
@primary_key {:id, :binary_id,
              autogenerate: true, null: false}
# id: binary()   (no | nil)
```

`:null` and `:enforce` work here too — they are stripped before Ecto sees them.

### `timestamps/1`

```elixir
timestamps()             # NaiveDateTime.t() | nil
timestamps(null: false)  # NaiveDateTime.t()
timestamps(type: :utc_datetime)  # DateTime.t() | nil
```

Nullable by default: a struct that was not inserted yet has `nil` timestamps.

### `belongs_to/3` foreign key

```elixir
belongs_to(:company, Company, null: false)
# company: Ecto.Schema.belongs_to(Company.t()) | nil
# company_id: integer()
```

`:null` applies to the generated foreign key; `:enforce` enforces the association field.

## Documenting fields
{: .col-2}

### The moduledoc marker

```elixir
defmodule Person do
  @moduledoc """
  A person.

  ## Fields

  <!-- typed_ecto_schema: fields -->
  """

  use TypedEctoSchema

  typed_schema "people" do
    field(:name, :string, doc: "The person's full name")
    field(:age, :integer)
  end
end
```

The marker is replaced at compile time with a list of every field, its typespec and its `:doc`:

```markdown
- `id` (`integer() | nil`)
- `name`: The person's full name (`String.t()`)
- `age` (`integer() | nil`)
```

The marker is the only trigger — without it, `:doc` options don't touch the `@moduledoc`.

### Controlling the generated `@typedoc`

```elixir
# Default: t/0 gets a @typedoc with the
# fields list, for every schema
typed_schema "people" do
  ...
end
```

```elixir
# Your own @typedoc is kept; the marker
# is interpolated in it too
@typedoc """
A person. Prefer building via new/1.

<!-- typed_ecto_schema: fields -->
"""
typed_schema "people" do
  ...
end
```

```elixir
# Suppress it entirely
@typedoc false
typed_schema "people" do
  ...
end
```

Field docs show up in `t Person.t()` in IEx and on hover in editors.

## Experimental features
{: .col-2}

### Named types for `Ecto.Enum`

```elixir
typed_schema "people", additional_types: true do
  field(:role, Ecto.Enum, values: [:admin, :user])
end
# @type role() :: :admin | :user
# usable as Person.role()
```

Or enable it globally (compile-time config; the schema-level option still wins in both directions):

```elixir
# config/config.exs
config :typed_ecto_schema, additional_types: true
```

For `{:array, Ecto.Enum}` the named type is the **element** union — write `list(Person.roles())` for the list.

### PolymorphicEmbed integration

```elixir
# config/config.exs (compile-time, off by default)
config :typed_ecto_schema, polymorphic_embed: true
```

```elixir
polymorphic_embeds_one(:channel,
  types: [sms: SMS, email: Email],
  on_replace: :update
)
# channel: (SMS.t() | Email.t()) | nil

polymorphic_embeds_many(:channels,
  types: [sms: SMS, email: Email],
  on_replace: :delete
)
# channels: list(SMS.t() | Email.t())
```

`::`, `:null`, `:enforce` and `:doc` work as on `field/3`. With `additional_types: true` these fields get named types too (element union for `_many`). `polymorphic_embed` stays out of this library's deps — add it to yours.
