Caravela.Domain (Caravela v0.13.1)

Copy Markdown View Source

DSL entry point. use Caravela.Domain in a module to declare a domain:

defmodule MyApp.Domains.Library do
  use Caravela.Domain

  entity :authors do
    field :name, :string, required: true
    field :bio, :text
  end

  entity :books do
    field :title, :string, required: true, min_length: 3
    field :isbn, :string, format: ~r/^\d{13}$/
  end

  relation :authors, :books, type: :has_many

  on_create :books, fn changeset, _context ->
    Ecto.Changeset.validate_required(changeset, [:title])
  end

  policy :books do
    scope fn q, actor ->
      if actor.role == :admin, do: q, else: where(q, [b], b.published)
    end

    allow :create, fn actor -> actor.role in [:admin, :editor] end
  end
end

After compilation the module exposes __caravela_domain__/0, returning the validated Caravela.Schema.Domain IR, plus __caravela_hook__/4 and the __caravela_policy_*__ clauses emitted by each policy block.

Summary

Functions

Declare an action gate for the enclosing policy block. action is one of :create, :update, :delete; the fn has arity 1 (actor -> bool) or arity 2 (actor, record -> bool).

Declare the authenticatable trait on the enclosing entity.

Enable email confirmation inside an authenticatable block.

Declare an entity (a table/schema) with a do block of fields.

Declare a field. Two call shapes are supported and dispatched on the second argument's AST

Declare a create-time hook for an entity. The hook receives the Ecto.Changeset and the caller's context map, and must return an Ecto.Changeset.

Declare a delete-time hook. The hook receives the loaded entity and the context, and must return :ok or {:error, reason}.

Custom post-login logic. Receives the loaded user and the caller's context, returns :ok or {:error, reason}.

Custom registration logic. Receives the changeset and the caller's context map, returns the changeset.

Declare an update-time hook. Same shape as on_create/2.

Declare a triple-target policy for entity.

Declare a relation between two entities.

Enable password reset inside an authenticatable block.

Declare the row-level scope for the enclosing policy block.

Configure session management inside an authenticatable block.

Declare a credential strategy inside an authenticatable block.

Declare the API version for this domain. Must match ~r/^v\d+$/.

Functions

allow(action, fun)

(macro)

Declare an action gate for the enclosing policy block. action is one of :create, :update, :delete; the fn has arity 1 (actor -> bool) or arity 2 (actor, record -> bool).

allow :create, fn actor -> actor.role in [:admin, :editor] end
allow :update, fn actor, record -> actor.id == record.author_id end

authenticatable(list)

(macro)

Declare the authenticatable trait on the enclosing entity.

entity :users do
  field :email, :string, required: true, unique: true
  field :name, :string, required: true

  authenticatable do
    strategy :password
    strategy :api_token, scopes: [:read, :write], ttl: {90, :days}
    session :token, ttl: {30, :days}, remember_me: {365, :days}
    confirm :email, token_ttl: {24, :hours}
    reset :password, token_ttl: {1, :hour}

    on_register fn changeset, _ctx -> changeset end
    on_login fn user, _ctx ->
      if user.suspended, do: {:error, :suspended}, else: :ok
    end
  end
end

See Caravela.Schema.AuthConfig for the parsed IR.

confirm(kind, opts \\ [])

(macro)

Enable email confirmation inside an authenticatable block.

entity(name, opts \\ [], list)

(macro)

Declare an entity (a table/schema) with a do block of fields.

entity :books do
  field :title, :string, required: true
end

Entity-level options may be passed between the name and the do block:

entity :books, frontend: :rest do
  field :title, :string, required: true
end

Supported options:

  • :frontend — render transport for generated UI. One of :live (default — LiveView + WebSocket) or :rest (Inertia-style HTTP via caravela_svelte). Entities stay on :live when the option is omitted, so existing domains are unaffected.

  • :realtime — opt into SSE-driven live updates on top of the :rest transport. Defaults to false. Only valid when frontend: :rest:live entities already have LiveView's WebSocket, so realtime: true there is rejected with a Caravela.DSLError. Generated controllers get a broadcast_patch/3 call site on create / update / delete.

    entity :books, frontend: :rest, realtime: true do
      field :title, :string, required: true
    end

field(name, second, opts \\ [])

(macro)

Declare a field. Two call shapes are supported and dispatched on the second argument's AST:

Entity field (inside entity do … end): second arg is a type atom.

field :title, :string, required: true, min_length: 3

Policy field rule (inside policy do … end): second arg is a keyword list with :visible.

field :price, visible: fn actor -> actor.role == :admin end
field :author_email,
  visible: fn actor, record -> actor.id == record.author_id end

The dispatch is a pure AST check — no runtime overhead.

on_create(entity, fun)

(macro)

Declare a create-time hook for an entity. The hook receives the Ecto.Changeset and the caller's context map, and must return an Ecto.Changeset.

on_create :books, fn changeset, _context ->
  Ecto.Changeset.validate_required(changeset, [:title])
end

on_delete(entity, fun)

(macro)

Declare a delete-time hook. The hook receives the loaded entity and the context, and must return :ok or {:error, reason}.

on_delete :authors, fn author, _context ->
  if author.published?, do: {:error, :has_published_books}, else: :ok
end

on_login(fun)

(macro)

Custom post-login logic. Receives the loaded user and the caller's context, returns :ok or {:error, reason}.

on_register(fun)

(macro)

Custom registration logic. Receives the changeset and the caller's context map, returns the changeset.

on_update(entity, fun)

(macro)

Declare an update-time hook. Same shape as on_create/2.

on_update :books, fn changeset, _context ->
  changeset
end

policy(entity, list)

(macro)

Declare a triple-target policy for entity.

policy :books do
  scope fn query, actor ->
    if actor.role == :admin, do: query, else: where(query, [b], b.published)
  end

  field :internal_notes, visible: fn actor -> actor.role == :admin end
  field :author_email,   visible: fn actor, record ->
    actor.role == :admin or actor.id == record.author_id
  end

  allow :create, fn actor -> actor.role in [:admin, :editor] end
  allow :update, fn actor, record ->
    actor.role == :admin or actor.id == record.author_id
  end
  allow :delete, fn actor -> actor.role == :admin end
end

The block is plain Elixir — for, if, helper function calls, and @module_attribute splicing all work the same as anywhere else:

policy :books do
  for f <- @admin_only_fields do
    field f, visible: fn actor -> actor.role == :admin end
  end

  if Mix.env() == :dev do
    allow :delete, fn _actor -> true end
  end
end

Each rule declaration stores metadata in the @caravela_policy_rules accumulator; Caravela.Compiler.__before_compile__/1 then assembles the full IR and emits the __caravela_policy_*__ dispatch clauses in one pass.

relation(from, to, opts)

(macro)

Declare a relation between two entities.

relation :authors, :books, type: :has_many
relation :books, :publishers, type: :belongs_to

reset(kind, opts \\ [])

(macro)

Enable password reset inside an authenticatable block.

scope(fun)

(macro)

Declare the row-level scope for the enclosing policy block.

scope fn query, actor ->
  if actor.role == :admin, do: query, else: where(query, [b], b.published)
end

The fn must have arity 2 — (query, actor) -> query. Raises at compile time if called outside a policy block.

session(kind, opts \\ [])

(macro)

Configure session management inside an authenticatable block.

strategy(name, opts \\ [])

(macro)

Declare a credential strategy inside an authenticatable block.

version(v)

(macro)

Declare the API version for this domain. Must match ~r/^v\d+$/.

version "v1"

When set, all generated modules are namespaced under the version (MyApp.Library.V1.Book) and controller routes are prefixed with /api/v1/.