# Ecto Context Module Template Copy-pasteable template for a new Ecto schema + context + test following TowerOps conventions. Replace `__CONTEXT__` with PascalCase context name (e.g., `Schedules`). Replace `__SCHEMA__` with PascalCase schema name (e.g., `Schedule`). Replace `__resource__` with snake_case (e.g., `schedule`). Replace `__resources__` with plural snake_case (e.g., `schedules`). Replace `__table__` with the database table name (e.g., `schedules`). --- ## `lib/towerops/__context__/__resource__.ex` (Schema) ```elixir defmodule Towerops.__CONTEXT__.__SCHEMA__ do @moduledoc """ Schema for __resources__. Describe what this resource represents and its relationships. """ use Ecto.Schema import Ecto.Changeset alias Towerops.Organizations.Organization @primary_key {:id, :binary_id, autogenerate: true} @foreign_key_type :binary_id schema "__table__" do field :name, :string field :description, :string field :enabled, :boolean, default: true belongs_to :organization, Organization timestamps() end @required_fields ~w(name organization_id)a @optional_fields ~w(description enabled)a def changeset(__resource__, attrs) do __resource__ |> cast(attrs, @required_fields ++ @optional_fields) |> validate_required(@required_fields) |> validate_length(:name, max: 255) |> validate_length(:description, max: 1000) |> foreign_key_constraint(:organization_id) end end ``` --- ## `lib/towerops/__context__.ex` (Context) ```elixir defmodule Towerops.__CONTEXT__ do @moduledoc """ Context for managing __resources__. """ import Ecto.Query alias Towerops.__CONTEXT__.__SCHEMA__ alias Towerops.Repo @preloads [:organization] def create___resource__(attrs) do %__SCHEMA__{} |> __SCHEMA__.changeset(attrs) |> Repo.insert() end def update___resource__(%__SCHEMA__{} = __resource__, attrs) do __resource__ |> __SCHEMA__.changeset(attrs) |> Repo.update() end def delete___resource__(%__SCHEMA__{} = __resource__) do Repo.delete(__resource__) end def get___resource__!(id) do __SCHEMA__ |> Repo.get!(id) |> Repo.preload(@preloads) end def list___resources__(organization_id, opts \\ []) do __SCHEMA__ |> where([r], r.organization_id == ^organization_id) |> apply_filter(opts[:filter]) |> order_by([r], desc: r.inserted_at) |> Repo.all() |> Repo.preload(@preloads) end defp apply_filter(query, :active) do where(query, [r], r.enabled == true) end defp apply_filter(query, :archived) do where(query, [r], r.enabled == false) end defp apply_filter(query, _), do: query end ``` --- ## Migration ```elixir defmodule Towerops.Repo.Migrations.Create__SCHEMA__s do use Ecto.Migration def change do create table(:__table__, primary_key: false) do add :id, :binary_id, primary_key: true add :name, :string, null: false add :description, :text add :enabled, :boolean, default: true, null: false add :organization_id, references(:organizations, type: :binary_id, on_delete: :delete_all), null: false timestamps() end create index(:__table__, [:organization_id]) end end ``` --- ## `test/towerops/__context___test.exs` (Context Tests) ```elixir defmodule Towerops.__CONTEXT__Test do use Towerops.DataCase alias Towerops.__CONTEXT__ alias Towerops.__CONTEXT__.__SCHEMA__ setup do user = insert_user() {:ok, organization} = Towerops.Organizations.create_organization(%{name: "Test Org"}, user.id) %{organization: organization, user: user} end describe "create___resource__/1" do test "creates with valid attrs", %{organization: org} do attrs = %{ name: "Test __RESOURCE_TITLE__", organization_id: org.id } assert {:ok, %__SCHEMA__{} = __resource__} = __CONTEXT__.create___resource__(attrs) assert __resource__.name == "Test __RESOURCE_TITLE__" assert __resource__.organization_id == org.id end test "fails without required fields" do assert {:error, changeset} = __CONTEXT__.create___resource__(%{}) assert errors_on(changeset) |> Map.has_key?(:name) assert errors_on(changeset) |> Map.has_key?(:organization_id) end end describe "update___resource__/2" do test "updates with valid attrs", %{organization: org} do {:ok, __resource__} = __CONTEXT__.create___resource__(%{name: "Original", organization_id: org.id}) assert {:ok, updated} = __CONTEXT__.update___resource__(__resource__, %{name: "Updated"}) assert updated.name == "Updated" end end describe "delete___resource__/1" do test "deletes the resource", %{organization: org} do {:ok, __resource__} = __CONTEXT__.create___resource__(%{name: "To Delete", organization_id: org.id}) assert {:ok, _} = __CONTEXT__.delete___resource__(__resource__) assert_raise Ecto.NoResultsError, fn -> __CONTEXT__.get___resource__!(__resource__.id) end end end describe "list___resources__/2" do test "lists resources for organization", %{organization: org} do {:ok, _} = __CONTEXT__.create___resource__(%{name: "Item 1", organization_id: org.id}) {:ok, _} = __CONTEXT__.create___resource__(%{name: "Item 2", organization_id: org.id}) __resources__ = __CONTEXT__.list___resources__(org.id) assert length(__resources__) == 2 end test "filters by active status", %{organization: org} do {:ok, _} = __CONTEXT__.create___resource__(%{name: "Active", organization_id: org.id, enabled: true}) {:ok, _} = __CONTEXT__.create___resource__(%{name: "Archived", organization_id: org.id, enabled: false}) active = __CONTEXT__.list___resources__(org.id, filter: :active) assert length(active) == 1 assert hd(active).name == "Active" end end describe "get___resource__!/1" do test "returns the resource with preloads", %{organization: org} do {:ok, created} = __CONTEXT__.create___resource__(%{name: "Find Me", organization_id: org.id}) __resource__ = __CONTEXT__.get___resource__!(created.id) assert __resource__.name == "Find Me" assert __resource__.organization.id == org.id end end end ```