Current section
Files
Jump to
Current section
Files
priv/templates/context.eex
defmodule <%= inspect @context_module %> do
@moduledoc """
Generated by Caravela from <%= inspect @domain_module %>.
CRUD context with hooks and triple-target policies (row scope +
field projection + action gates).<%= if @multi_tenant do %>
Multi-tenant: every read is scoped by `tenant_id` and every write
stamps `tenant_id` from the caller's `context.tenant`.<% end %>
Regenerate with `mix caravela.gen.context <%= inspect @domain_module %>`.
Custom code placed below the `# --- CUSTOM ---` marker is preserved.
"""
<%= if @multi_tenant do %> import Ecto.Query, only: [where: 3]
<% end %><%= if @any_preloads do %> import Ecto.Query, only: [preload: 2]
<% end %>
alias <%= inspect @repo_module %>
<%= for e <- @entities do %> alias <%= inspect e.module %>
<% end %>
<%= for e <- @entities do %>
@doc "List `<%= e.plural %>` the caller is allowed to read."
def <%= e.list_fn %>(context \\ %{}) do
<%= e.module_short %>
|> scope_tenant(context)
|> apply_scope(<%= inspect e.entity_name %>, context)<%= if e.preloads != [] do %>
|> preload(<%= inspect e.preloads %>)<% end %>
|> Repo.all()
|> project_fields(<%= inspect e.entity_name %>, context)
end
# --- CUSTOM :<%= e.list_fn %> ---
# --- END :<%= e.list_fn %> ---
@doc "Fetch a single `<%= e.singular %>` by id. Returns `nil` if not found or not visible."
def <%= e.get_fn %>(id, context \\ %{}) do
<%= e.module_short %>
|> scope_tenant(context)
|> apply_scope(<%= inspect e.entity_name %>, context)<%= if e.preloads != [] do %>
|> preload(<%= inspect e.preloads %>)<% end %>
|> Repo.get(id)
|> project_field(<%= inspect e.entity_name %>, context)
end
# --- CUSTOM :<%= e.get_fn %> ---
# --- END :<%= e.get_fn %> ---
@doc "Fetch a single `<%= e.singular %>` by id. Raises if not found or not visible."
def <%= e.get_bang_fn %>(id, context \\ %{}) do
<%= e.module_short %>
|> scope_tenant(context)
|> apply_scope(<%= inspect e.entity_name %>, context)<%= if e.preloads != [] do %>
|> preload(<%= inspect e.preloads %>)<% end %>
|> Repo.get!(id)
|> project_field(<%= inspect e.entity_name %>, context)
end
@doc "Build an unsaved `<%= e.singular %>` changeset, for use in forms."
def <%= e.change_fn %>(%<%= e.module_short %>{} = <%= e.singular %>, attrs \\ %{}) do
<%= e.module_short %>.changeset(<%= e.singular %>, attrs)
end
@doc "Create a `<%= e.singular %>`: authorize, apply on_create hook, insert."
def <%= e.create_fn %>(attrs, context \\ %{}) do
with :ok <- policy_authorize(<%= inspect e.entity_name %>, :create, context) do
%<%= e.module_short %>{}
|> <%= e.module_short %>.changeset(attrs)
|> inject_tenant_id(context)
|> apply_changeset_hook(:on_create, <%= inspect e.entity_name %>, context)
|> Repo.insert()
end
end
# --- CUSTOM :<%= e.create_fn %> ---
# --- END :<%= e.create_fn %> ---
@doc "Update a `<%= e.singular %>`: authorize, apply on_update hook, update."
def <%= e.update_fn %>(%<%= e.module_short %>{} = <%= e.singular %>, attrs, context \\ %{}) do
with :ok <- policy_authorize(<%= inspect e.entity_name %>, :update, <%= e.singular %>, context) do
<%= e.singular %>
|> <%= e.module_short %>.changeset(attrs)
|> apply_changeset_hook(:on_update, <%= inspect e.entity_name %>, context)
|> Repo.update()
end
end
# --- CUSTOM :<%= e.update_fn %> ---
# --- END :<%= e.update_fn %> ---
@doc """
Delete a `<%= e.singular %>`: authorize, run on_delete hook, delete.
Accepts either the loaded struct (skipping the read round-trip) or
the primary-key id (one `get` + one `delete`). Returns
`{:error, :not_found}` when called by id and the row is invisible
or missing.
"""
def <%= e.delete_fn %>(struct_or_id, context \\ %{})
def <%= e.delete_fn %>(%<%= e.module_short %>{} = <%= e.singular %>, context) do
with :ok <- policy_authorize(<%= inspect e.entity_name %>, :delete, <%= e.singular %>, context),
:ok <- run_delete_hook(<%= inspect e.entity_name %>, <%= e.singular %>, context) do
Repo.delete(<%= e.singular %>)
end
end
def <%= e.delete_fn %>(id, context) when is_binary(id) or is_integer(id) do
case <%= e.get_fn %>(id, context) do
nil -> {:error, :not_found}
entity -> <%= e.delete_fn %>(entity, context)
end
end
# --- CUSTOM :<%= e.delete_fn %> ---
# --- END :<%= e.delete_fn %> ---
<% end %>
# --- Field access metadata -----------------------------------------------
#
# Computed once per request and exposed both as JSON projection input
# (via `project_fields/3`) and as a typed Svelte prop (via
# `field_access/2`, called from generated LiveViews).
@doc """
Returns the `field_access` map for `entity` — keys are field atoms,
values are `true`, `false`, or `:per_record` (record-dependent rule;
evaluate per row).
"""
def field_access(entity, context) do
actor = actor_from_context(context)
compute_field_access(entity, actor)
end
<%= for e <- @entities do %> defp compute_field_access(<%= inspect e.entity_name %>, actor) do
%{
<%= for f <- e.field_access_exprs do %> <%= inspect f.name %> => <%= f.expr %>,
<% end %> }
end
<% end %> defp compute_field_access(_entity, _actor), do: %{}
# --- Internal: policy + hook dispatch ------------------------------------
defp apply_scope(query, entity, context) do
<%= inspect @domain_module %>.__caravela_policy_scope__(entity, query, actor_from_context(context))
end
defp apply_changeset_hook(changeset, action, entity, context) do
<%= inspect @domain_module %>.__caravela_hook__(action, entity, changeset, context)
end
<%= if @any_on_delete do %>
defp run_delete_hook(entity, struct, context) do
case <%= inspect @domain_module %>.__caravela_hook__(:on_delete, entity, struct, context) do
:ok -> :ok
{:error, _} = err -> err
other -> raise "on_delete hook must return :ok or {:error, reason}, got: " <> inspect(other)
end
end
<% else %>
defp run_delete_hook(_entity, _struct, _context), do: :ok
<% end %>
defp policy_authorize(entity, action, context) do
if <%= inspect @domain_module %>.__caravela_policy_allow__(
entity,
action,
actor_from_context(context)
) == true do
:ok
else
{:error, :unauthorized}
end
end
defp policy_authorize(entity, action, record, context) do
if <%= inspect @domain_module %>.__caravela_policy_allow__(
entity,
action,
actor_from_context(context),
record
) == true do
:ok
else
{:error, :unauthorized}
end
end
defp actor_from_context(context) do
context
|> Map.get(:current_user)
|> Kernel.||(Map.get(context, "current_user"))
end
# List-shaped projection for `list_*`. Strips invisible fields per row.
defp project_fields(records, entity, context) when is_list(records) do
field_access = compute_field_access(entity, actor_from_context(context))
Enum.map(records, &do_project(&1, field_access, entity, context))
end
# Single-record projection used by `get_*`. Handles `nil` cleanly.
defp project_field(nil, _entity, _context), do: nil
defp project_field(record, entity, context) do
field_access = compute_field_access(entity, actor_from_context(context))
do_project(record, field_access, entity, context)
end
defp do_project(record, field_access, entity, context) do
actor = actor_from_context(context)
Enum.reduce(field_access, record, fn {field, visibility}, acc ->
case visibility do
true ->
acc
false ->
redact(acc, field)
:per_record ->
if <%= inspect @domain_module %>.__caravela_policy_field_visible__(entity, field, actor, acc) do
acc
else
redact(acc, field)
end
end
end)
end
# Works for both Ecto structs and plain maps — the client-bound
# representation may be either depending on controller/LiveSvelte
# serialisation. We set the field to `nil` rather than removing it,
# so the TypeScript shape stays stable for Svelte components.
defp redact(%_struct{} = record, field) do
if Map.has_key?(record, field), do: Map.put(record, field, nil), else: record
end
defp redact(record, field) when is_map(record) do
Map.put(record, field, nil)
end
defp redact(record, _field), do: record
# --- Internal: multi-tenant scoping --------------------------------------
<%= if @multi_tenant do %>
defp scope_tenant(query, %{tenant: %{id: tenant_id}}) when not is_nil(tenant_id) do
where(query, [q], q.tenant_id == ^tenant_id)
end
defp scope_tenant(query, _context), do: query
defp inject_tenant_id(changeset, %{tenant: %{id: tenant_id}}) when not is_nil(tenant_id) do
Ecto.Changeset.put_change(changeset, :tenant_id, tenant_id)
end
defp inject_tenant_id(changeset, _context), do: changeset
<% else %>
defp scope_tenant(query, _context), do: query
defp inject_tenant_id(changeset, _context), do: changeset
<% end %>
<%= @custom_marker %>