defmodule <%= inspect @controller_module %> do @moduledoc """ Generated by Caravela from <%= inspect @domain_module %>. Renders Svelte components via `caravela_svelte`'s Inertia-style transport. Every action delegates CRUD to `<%= inspect @context_module %>` (authorization, hooks, and — when enabled — tenant scoping apply automatically) and ships the entity's policy-derived `field_access` map as a prop so the Svelte component can render permission-gated fields without a round-trip.<%= if @realtime do %> `realtime: true` on the entity means `create`, `update`, and `delete` publish a JSON-Patch on the conventional per-actor topic via `CaravelaSvelte.Caravela.broadcast_patch/3`. Clients subscribed through `CaravelaSvelte.SSE` receive live updates.<% end %> Regenerate with `mix caravela.gen.live <%= inspect @domain_module %>`. Custom code placed below the `# --- CUSTOM ---` marker is preserved. """ use Phoenix.Controller, formats: [:html] alias <%= inspect @context_module %> alias Caravela.ChangesetTranslator alias CaravelaSvelte.Caravela, as: CS def index(conn, _params) do context = build_context(conn) items = <%= @context_short %>.<%= @list_fn %>(context) field_access = field_access(context) actions = <%= @context_short %>.action_access(<%= inspect @entity_name %>, context) conn |> CS.put_field_access(field_access) |> CaravelaSvelte.render(<%= inspect @component_index %>, %{ <%= @plural %>: items, field_access: field_access, actions: actions }) end # --- CUSTOM :index_<%= @singular %> --- # --- END :index_<%= @singular %> --- def show(conn, %{"id" => id}) do context = build_context(conn) case <%= @context_short %>.<%= @get_fn %>(id, context) do nil -> send_not_found(conn) entity -> field_access = field_access(context) actions = <%= @context_short %>.action_access(<%= inspect @entity_name %>, entity, context) conn |> CS.put_field_access(field_access) |> CaravelaSvelte.render(<%= inspect @component_show %>, %{ <%= @singular %>: entity, field_access: field_access, actions: actions }) end end # --- CUSTOM :show_<%= @singular %> --- # --- END :show_<%= @singular %> --- def new(conn, _params) do context = build_context(conn) changeset = <%= @context_short %>.<%= @change_fn %>(%<%= inspect @entity_module %>{}, %{}, context) field_access = field_access(context) actions = <%= @context_short %>.action_access(<%= inspect @entity_name %>, context) conn |> CS.put_field_access(field_access) |> CaravelaSvelte.render(<%= inspect @component_form %>, %{ <%= @singular %>: changeset.data, errors: %{}, action: "create", field_access: field_access, actions: actions }) end # --- CUSTOM :new_<%= @singular %> --- # --- END :new_<%= @singular %> --- def edit(conn, %{"id" => id}) do context = build_context(conn) case <%= @context_short %>.<%= @get_fn %>(id, context) do nil -> send_not_found(conn) entity -> field_access = field_access(context) actions = <%= @context_short %>.action_access(<%= inspect @entity_name %>, entity, context) conn |> CS.put_field_access(field_access) |> CaravelaSvelte.render(<%= inspect @component_form %>, %{ <%= @singular %>: entity, errors: %{}, action: "update", field_access: field_access, actions: actions }) end end # --- CUSTOM :edit_<%= @singular %> --- # --- END :edit_<%= @singular %> --- def create(conn, params) do context = build_context(conn) attrs = Map.get(params, "<%= @singular %>", Map.drop(params, ["id"])) case <%= @context_short %>.<%= @create_fn %>(attrs, context) do {:ok, entity} -><%= if @realtime do %> broadcast(entity, context, :create) <% end %> redirect(conn, to: "<%= @index_path %>/" <> to_string(entity.id)) {:error, :unauthorized} -> send_unauthorized(conn) {:error, %Ecto.Changeset{} = changeset} -> field_access = field_access(context) actions = <%= @context_short %>.action_access(<%= inspect @entity_name %>, context) conn |> CS.put_field_access(field_access) |> put_status(:unprocessable_entity) |> CaravelaSvelte.render(<%= inspect @component_form %>, %{ <%= @singular %>: changeset.data, errors: ChangesetTranslator.translate(changeset), action: "create", field_access: field_access, actions: actions }) {:error, reason} -> send_error(conn, reason) end end # --- CUSTOM :create_<%= @singular %> --- # --- END :create_<%= @singular %> --- def update(conn, %{"id" => id} = params) do context = build_context(conn) attrs = Map.get(params, "<%= @singular %>", Map.drop(params, ["id"])) case <%= @context_short %>.<%= @get_fn %>(id, context) do nil -> send_not_found(conn) entity -> case <%= @context_short %>.<%= @update_fn %>(entity, attrs, context) do {:ok, updated} -><%= if @realtime do %> broadcast(updated, context, :update) <% end %> redirect(conn, to: "<%= @index_path %>/" <> to_string(updated.id)) {:error, :unauthorized} -> send_unauthorized(conn) {:error, %Ecto.Changeset{} = changeset} -> field_access = field_access(context) actions = <%= @context_short %>.action_access(<%= inspect @entity_name %>, entity, context) conn |> CS.put_field_access(field_access) |> put_status(:unprocessable_entity) |> CaravelaSvelte.render(<%= inspect @component_form %>, %{ <%= @singular %>: entity, errors: ChangesetTranslator.translate(changeset), action: "update", field_access: field_access, actions: actions }) {:error, reason} -> send_error(conn, reason) end end end # --- CUSTOM :update_<%= @singular %> --- # --- END :update_<%= @singular %> --- def delete(conn, %{"id" => id}) do context = build_context(conn) case <%= @context_short %>.<%= @get_fn %>(id, context) do nil -> send_not_found(conn) entity -> case <%= @context_short %>.<%= @delete_fn %>(entity, context) do {:ok, _} -><%= if @realtime do %> broadcast(entity, context, :delete) <% end %> redirect(conn, to: "<%= @index_path %>") {:error, :unauthorized} -> send_unauthorized(conn) {:error, reason} -> send_error(conn, reason) end end end # --- CUSTOM :delete_<%= @singular %> --- # --- END :delete_<%= @singular %> --- # --- Internal -------------------------------------------------------- defp build_context(conn) do %{ current_user: conn.assigns[:current_user],<%= if @multi_tenant do %> tenant: conn.assigns[:tenant],<% end %> conn: conn } end # Placeholder — slice 3 will derive this from the compiled policy. # Kept here so generated code has a single call-site to extend. defp field_access(_context), do: %{} <%= if @realtime do %> defp broadcast(%<%= inspect @entity_module %>{} = <%= @singular %>, context, op) do ops = patch_ops(<%= @singular %>, op) actor_id = context |> Map.get(:current_user) |> actor_id() CS.broadcast_patch(<%= inspect @entity_short %>, actor_id, ops) end defp patch_ops(<%= @singular %>, :create), do: [%{"op" => "add", "path" => "/<%= @plural %>/-", "value" => <%= @singular %>}] defp patch_ops(<%= @singular %>, :update), do: [%{"op" => "replace", "path" => "/<%= @plural %>/" <> to_string(<%= @singular %>.id), "value" => <%= @singular %>}] defp patch_ops(<%= @singular %>, :delete), do: [%{"op" => "remove", "path" => "/<%= @plural %>/" <> to_string(<%= @singular %>.id)}] defp actor_id(nil), do: nil defp actor_id(%{id: id}), do: id defp actor_id(_), do: nil <% end %> defp send_not_found(conn), do: conn |> put_status(:not_found) |> text("not_found") defp send_unauthorized(conn), do: conn |> put_status(:forbidden) |> text("unauthorized") defp send_error(conn, reason), do: conn |> put_status(:unprocessable_entity) |> text(inspect(reason)) <%= @custom_marker %>