We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
Post 92
LiveView 07 - Chapter 3 Understand The Generated boundary and Your Turn
Published on: 2025-12-09
Tags:
elixir, Blog, Side Project, Testing, LiveView, Ecto, Html/CSS, Phoenix, mix format, pre-commit
Understand The Generated boundary
Context is the boundary from the real world to the un-sanitized formats of the world. The Context has at least 3 responsibilities:
Access External services
Abstract Away Tedious Details
Handle Uncertainty
Present a single, common API
Let's start to break these down
Access External Services
Even the DB is an external service. Catalog context provides the service of Database Access. That is why the schema changeset code is in the schema as it will give us exact ways of adding in data. There is an other way to access the DB and that is with queries, these will not always yield the results that we want, it is unpredictable. Ecto uses repo for this and and thing that uses it should be in the context.
There are some built in features of Phoenix that uses the scope to be sure that a user can only see the data that they should see. Or delete only the products that they should be able to delete.
Abstract Away Tedious Details
We want to make Ecto work as much as possible, cast and validate will help with this. Let's look at how the new Phoenix creates a product. pento/lib/pento/catalog.ex
def create_product(%Scope{} = scope, attrs) do
with {:ok, product = %Product{}} <-
%Product{}
|> Product.changeset(attrs, scope)
|> Repo.insert() do
broadcast_product(scope, {:created, product})
{:ok, product}
end
end
Scope is the first param and with the plugs that we have setup we will always have the scope within the attributes. Check out this way in which we might deal with a create product event. You will not see the say thing withing the current project as they use forms to create a product but check this out.
# In a LiveView or controller, scope is available from the socket/conn
def handle_event("create_product", product_params, socket) do
case Catalog.create_product(socket.assigns.current_scope, product_params) do
{:ok, product} ->
# Product automatically belongs to the current user
{:noreply, put_flash(socket, :info, "Product created!")}
{:error, changeset} ->
{:noreply, assign(socket, :changeset, changeset)}
end
end
This makes it so that we always can add in the user to the product being made and then we can leverage that to make it so only those that own the product can change it.
Present a Single, Common API
We don't want anyone to be able to access the schema, we want to wrap in in a function to make sure that the data is sanitized and ready for any task. Head to pento/lib/pent/catalog.ex
def change_product(%Scope{} = scope, %Product{} = product, attrs \\ %{}) do
true = product.user_id == scope.user.id
Product.changeset(product, attrs, scope)
end
It might seem pointless but this will help to be sure that we are not having a client in our schema. Plus adding in the first line will be sure that you only can change the products that you are able to match the user id for.
Handle Uncertainty
We also need to be sure that we can translate unverified user input into data that we can use. That is why in the file pento/lib/pento/catalog.ex we use the changeset to validate.
def update_product(%Scope{} = scope, %Product{} = product, attrs) do
true = product.user_id == scope.user.id
with {:ok, product = %Product{}} <-
product
|> Product.changeset(attrs, scope)
|> Repo.update() do
broadcast_product(scope, {:updated, product})
{:ok, product}
end
end
def create_product(%Scope{} = scope, attrs) do
with {:ok, product = %Product{}} <-
%Product{}
|> Product.changeset(attrs, scope)
|> Repo.insert() do
broadcast_product(scope, {:created, product})
{:ok, product}
end
end
This will try to save a changeset but if it fails it will return the error with the current changeset, this will populate the form with the current state and then give the reason for the error.
Use the Context to Seed the Database
We will want to be able to build the database with some starting seeds to help test data and other features, head to pento/priv/repo/seeds.exs and add this into it.
alias Pento.{Accounts, Catalog}
# Create a seed user for development
# Create or fetch the seed user
user =
case Accounts.register_user(%{
email: "seed@example.com",
password: "password123password123"
}) do
{:ok, user} ->
IO.puts("Created user: #{user.email}")
user
{:error, _changeset} ->
# User already exists — fetch instead
Accounts.get_user_by_email(
"seed@example.com"
)
end
IO.puts("Seeding products for user: #{user.id}")
# Get the scope for this user
scope = Accounts.get_scope_for_user(user.id)
# Create sample products using the scope-aware context
products = [
%{
name: "Chess",
description: "The classic strategy game",
unit_price: 10.00,
sku: 5_678_910
},
%{
name: "Checkers",
description: "A classic board game",
unit_price: 8.00,
sku: 1_234_567
},
%{
name: "Backgammon",
description: "An ancient strategy game",
unit_price: 15.00,
sku: 9_876_543
}
]
Enum.each(products, fn product_attrs ->
{:ok, product} = Catalog.create_product(scope, product_attrs)
IO.puts("Created product: #{product.name}")
end)
Now execute the file with.
mix run priv/repo.seeds.exs
### quick thing here there is not get_scope_for_user within the accounts so I created it.
@doc """
Gets the scope for a given user ID. This is needed for the scope-aware contexts, that we are using
in the seeds file and elsewhere.
"""
def get_scope_for_user(user_id) do
%Pento.Accounts.Scope{user: get_user!(user_id)}
end
### Also the above seeds.exs is not the same as in the book as the one in the book failed after creating the user so I had to add in a case for when you already have a user created.
Boundary, Core, or Script?
Now that we have the some basic outlines let's talk about where things should go in the future.
Boundary/Context - Things that deal input/output, or Uncertainty
Core/Domain - Certainty or set goals every time
Scripts - Things outside these 2 scopes
The Context API is With-land
The Context or the API is the boundary of the service. Boundaries are there to handle uncertainty, one of the responsibilities is to handle database interactions, they can fail.
With this in mind think about the with-land functionality of elixir. This is the with, you have a set of routines that will run if the initial condition is met, then a land that will run if the with fails at any step. Let's look at an example of a bad use of pipes then the better use using with-land.
# this is a bad idea!
defmodule Pento.Catalog do
alias Catalog.Coupon.Validator
alias Catalog.Coupon
defp validate_code(code) do
# will return an :ok, *or* an :error tuple
{:ok, code} = Validator.validate_code(code)
code
end
defp calculate_new_total(code, purchase_total) do
# will return an :ok, *or* an :error tuple
Coupon.calculate_new_total(code, purchase_total)
end
def apply_coupon_code(code, purchase_total) do
code
|> validate_code
|> calculate_new_total(purchase_total)
end
end
This is meant to take a code and then validate it then calculate the new total, at any point the function can fail which would lead to errors being returned and having more and more cases of needing to deal with those errors. Now let's try that with an with-land
defmodule Pento.Catalog do
alias Catalog.Coupon.Validator
alias Catalog.Coupon
defp validate_code(code) do
# will return an :ok, *or* an :error tuple
{:ok, code} = Validator.validate_code(code)
code
end
defp calculate_new_total(code, purchase_total) do
# will return an :ok, *or* an :error tuple
Coupon.calculate_new_total(code, purchase_total)
end
def apply_coupon_code(code, purchase_total) do
with {:ok, code} <- validate_coupon(code),
{:ok, new_total} <- calculate_new_total(code, purchase_total) do
new_total
else
{:error, reason} ->
IO.puts "Error applying coupon: #{reason}"
_ ->
IO.puts "Unknown error applying coupon."
end
end
end
This deals with all the ways the before module could take all the while making a clean ui for the next coder. As this deals with uncertainty with-land really only belong in the boundary layer and then pipes should belong in the core/domain.
The Core is Pipe-land
The core is predictable and reliable. Build a query in the core and execute it in the boundary.
Schemas don't actually interact with the database. Instead think of them as road maps that describe to to tie one Elixir module to a database table. It just takes data and answers these questions:
What is the name of the table?
What are the fields the schema supports?
What are the relationships between the tables?
Operations Code
If you need code to support the development, deployment, or testing they go in the /priv. If it deals with the database it goes in the /priv/repo. Mix configurations go in the mix.exs. Configuration of the main environment goes in /config.
Your Turn
Generated code is great for starting off the CRUD for any new product. The layers it creates is a great way to sort and keep everything within the right "context." Core and boundary code is meant to keep certainty "core" away from uncertainty "boundary."
Give It a Try
Create another changeset in the Product Schema that only changes the unit_price field and only allows for a price decrease from the current price.
This one is pretty simple once you know that you can set your own private functions to test for the things that you need. Here is the changeset and the private function to test whether the new price is less than the older price, pento/lib/pento/catalog/product.ex
def changeset_unit_price(product, attrs) do
product
|> cast(attrs, [:unit_price])
|> validate_required([:unit_price])
|> validate_number(:unit_price, greater_than: 0.0)
|> less_than_current(:unit_price)
end
defp less_than_current(%Ecto.Changeset{} = changeset, field) do
current = Map.get(changeset.data, field)
new = get_field(changeset, field)
if new > current do
add_error(changeset, field, "must be less than or equal to current value")
else
changeset
end
end
Create a context function called markdown_product/2 that takes in an argument of the product and the amount by which the price should change. Use the new changeset that we just created to update the product.
Once you have the changeset its only a matter of making sure that you are not only using the changeset but also making sure that you are updating the database. Just so we know where everything is going here is the location of the file. pento/lib/pento/catalog.ex
@doc """
Marks down the unit_price of a product.
## Examples
iex> markdown_product(scope, product, %{unit_price: new_price})
{:ok, %Product{}}
iex> markdown_product(scope, product, %{unit_price: bad_price})
{:error, %Ecto.Changeset{}}
"""
def markdown_product(%Scope{} = scope, %Product{} = product, amount_decrease) do
true = product.user_id == scope.user.id
current_price = product.unit_price
attrs = %{
unit_price: current_price - amount_decrease
}
with {:ok, product = %Product{}} <-
product
|> Product.changeset_unit_price(attrs)
|> Repo.update() do
broadcast_product(scope, {:updated, product})
{:ok, product}
end
end
One last thing that I wanted to go over here is formatting. I want to add in a check before I commit anything that the project is formatted correctly.
# This is the way to format with mix
mix format
# You can add in a pre-commit hook with
mix format --check-formatted
Then you need to create the pre-commit file (.git/hooks/pre-commit.ex)
#!/bin/sh
mix format --check-formatted
if [ $? -ne 0 ]; then
echo "Code is not formatted. Run 'mix format' first."
exit 1
fi
You might need to find the .git directory as it might be hidden in your editor. Git also has these files already but will append .sample so make sure to remove that. Once this is all done make sure that the file is executable with
chmod +x .git/hooks/pre-commit
# Now you will be told if any file is not formatted that you need to format
# this is the new order
mix format
git status
git add .
git commit .
Last bit of detail before we move on. I wanted to get rid of a few things in the layouts.ex as I didn't want to see the phoenix logo and the links to all the Phoenix websites. I first had to understand that there is a needed wrapper of
<Layouts.app flash={@flash} current_scope={@current_scope}></Layouts.app>
If you wrap your render in this it will pull from that pulls from pento/lib/pento_web/components/layouts.ex I had to remove some of the code to get rid of the links that I didn't want to use.
@doc """
Renders your app layout.
This function is typically invoked from every template,
and it often contains your application menu, sidebar,
or similar.
## Examples
<Layouts.app flash={@flash}>
<h1>Content</h1>
</Layouts.app>
"""
attr :flash, :map, required: true, doc: "the map of flash messages"
attr :current_scope, :map,
default: nil,
doc: "the current [scope](https://hexdocs.pm/phoenix/scopes.html)"
slot :inner_block, required: true
def app(assigns) do
~H"""
<header class="navbar px-4 sm:px-6 lg:px-8">
<%!-- <div class="flex-1">
<a href="/" class="flex-1 flex w-fit items-center gap-2">
<img src={~p"/images/logo.svg"} width="36" />
<span class="text-sm font-semibold">v{Application.spec(:phoenix, :vsn)}</span>
</a>
</div> --%>
<div class="flex-1 flex justify-end">
<ul class="flex flex-column px-1 space-x-4 items-right">
<%!-- <li>
<a href="https://phoenixframework.org/" class="btn btn-ghost">Website</a>
</li> --%>
<%!-- <li>
<a href="https://github.com/phoenixframework/phoenix" class="btn btn-ghost">GitHub</a>
</li> --%>
<li>
<.theme_toggle />
</li>
<%!-- <li>
<a href="https://hexdocs.pm/phoenix/overview.html" class="btn btn-primary">
Get Started <span aria-hidden="true">→</span>
</a>
</li> --%>
</ul>
</div>
</header>
<main class="px-4 py-20 sm:px-6 lg:px-8">
<div class="mx-auto max-w-2xl space-y-4">
{render_slot(@inner_block)}
</div>
</main>
<.flash_group flash={@flash} />
"""
end
This is what I ended up turning it into.