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 %>