DustEcto.Schema (DustEcto v0.1.2)

Copy Markdown View Source

use DustEcto.Schema, prefix: ["links"], required: [:slug, :title] pairs an Ecto.Schema (embedded) with a Dust prefix and the slug field used as the per-record namespace key.

Usage

defmodule MyApp.Reading.Link do
  use DustEcto.Schema,
    prefix: ["links"],                # required: segment list
    required: [:slug, :title, :url],  # used by changeset + Repo.all guard
    mode: :flat                       # :flat (default) | :map

  embedded_schema do
    field :title, :string
    field :url, :string
    field :note, :string
  end

  def changeset(link, attrs) do
    link
    |> cast(attrs, [:slug, :title, :url, :note])
    |> validate_required(__dust_required_fields__())
    |> validate_dust_slug(:slug)
  end
end

Multi-segment prefixes are a non-empty list of segments. Earlier versions accepted a dotted string ("reading.links"); after the segment-first migration, this must be an explicit list (["reading", "links"]) so dots in segments survive intact.

Storage modes

Pick :flat (the default) unless you know you want :map.

:flat (default) — one PUT per field; record lives on the wire as N leaves at <prefix>/<slug>/<field> (canonical slash form). This is the natural Dust shape: other writers (MCP, curl, sibling clients) can edit a single field without knowing the rest of the record, and per-field subscriptions are granular. Cost: writes are not atomic across fields; a partial update is observable until the last PUT lands.

:map — one PUT for the whole record at <prefix>/<slug>, with the dumped struct as the value. Atomic, single revision per record. Cost: writes from outside the schema (a curl that PUTs links/foo/title directly) race with the next :map write that clobbers the whole record. Use when you are the only writer and you need whole-record atomicity.

Reads work identically in both modes — Dust stores everything as flat leaves on disk, and Repo.get/2 GETs the slug path which the server assembles back to a map.

What the macro provides

  • use Ecto.Schema + import Ecto.Changeset
  • @primary_key {:slug, :string, autogenerate: false}
  • __dust_prefix__/0 — the prefix segment list (["reading", "links"])
  • __dust_mode__/0:flat (default) or :map
  • __dust_required_fields__/0 — the :required list, used by both the user's validate_required and DustEcto.Repo.all/1's read-time guard so they stay in sync. Necessary because Ecto's validate_required is a runtime check with no introspectable metadata.
  • validate_dust_slug/2 — closes path-shape footguns by rejecting empty slugs, slugs containing . (would mis-shape a legacy record path; harmless under segment-first storage but still rejected for clarity), and slugs containing / (would conflict with the canonical slash separator).

Summary

Functions

Validates that the given slug field is a non-empty string.

Functions

validate_dust_slug(changeset, field)

@spec validate_dust_slug(Ecto.Changeset.t(), atom()) :: Ecto.Changeset.t()

Validates that the given slug field is a non-empty string.

In capver 3 (segment-first paths), the slug is one path segment. Literal . and / inside that segment are ordinary characters: the dot is just a dot, and the slash is rendered as ~1 per RFC 6901 before hitting any wire / SQL key. So this validator only rejects what's structurally invalid — empty strings and non-strings.

Use inside any changeset/2:

|> validate_dust_slug(:slug)