Overview
View SourceAurora.Ctx is a code generation library that implements the Context pattern in Phoenix applications. It automatically generates standardized database interaction functions following Phoenix's architectural guidelines, while maintaining full compatibility with Ecto's query composition and changesets. The library supports both standard CRUD operations and advanced query features like filtering, sorting, and pagination.
Key Benefits
- Reduced Boilerplate: Automatically generates common database access functions
- Consistent API: Ensures uniform function names and patterns across contexts
- Type Safety: All generated functions include proper typespecs
- Flexible: Supports customization through options and hooks
- Maintainable: Less code to maintain, centralized logic
Getting Started
Add aurora_ctx
to your list of dependencies in mix.exs
:
def deps do
[
{:aurora_ctx, "~> 0.1.0"}
]
end
Basic Usage
Creating an Ecto Schema
Tip: Consider using TypedEctoSchema to reduce boilerplate in your schema definitions and add compile-time type checking.
Let's start with a typical Ecto schema definition that we'll use throughout the examples:
defmodule MyApp.Inventory.Product do
use Ecto.Schema
schema "products" do
field :name, :string
field :price, :decimal
timestamps()
end
def changeset(product, attrs) do
product
|> cast(attrs, [:name, :price])
|> validate_required([:name, :price])
end
end
Traditional Context Approach
Without Aurora.Ctx, you would typically write a context module with all the CRUD operations manually:
defmodule MyApp.Inventory do
import Ecto.Query
alias MyApp.Repo
alias MyApp.Inventory.Product
def list_products do
Product |> from() |> Repo.all()
end
def get_product(id) do
Product |> Repo.get(id)
end
def get_product!(id) do
Product |> Repo.get!(id)
end
def create_product(attrs) do
%Product{}
|> Product.changeset(attrs)
|> Repo.insert()
end
def update_product(%Product{} = product, attrs) do
product
|> Product.changeset(attrs)
|> Repo.update()
end
def delete_product(%Product{} = product) do
Repo.delete(product)
end
def change_product(%Product{} = product, attrs \\ %{}) do
Product.changeset(product, attrs)
end
end
Context Generation with Aurora.Ctx
With Aurora.Ctx, the same functionality can be achieved with just a few lines of code:
defmodule MyApp.Inventory do
use Aurora.Ctx
# The repository will default to MyApp.Repo
ctx_register_schema(MyApp.Inventory.Product)
end
Now you can use the context just like the traditional approach:
# List all products
products = Inventory.list_products()
# Get a specific product
product = Inventory.get_product!(1)
# Create a new product
{:ok, product} = Inventory.create_product(%{name: "Widget", price: 19.99})
# Update a product
{:ok, product} = Inventory.update_product(product, %{price: 29.99})
# Delete a product
{:ok, _deleted} = Inventory.delete_product(product)
Function Customization
You can override any generated function by defining it before calling ctx_register_schema
:
defmodule MyApp.Inventory do
use Aurora.Ctx
def list_products do
# Custom implementation with specific business logic
MyApp.Inventory.Product
|> where([p], p.active == true)
|> order_by([p], [desc: p.inserted_at])
|> MyApp.Repo.all()
end
ctx_register_schema(MyApp.Inventory.Product)
end
Generated Functions
For each schema, the following functions are automatically generated:
List Functions
list_*()
- List all recordslist_*(opts)
- List all records with optionslist_*_paginated()
- List records with paginationlist_*_paginated(opts)
- List records with pagination and optionscount_*()
- Count total recordscount_*(opts)
- Count records with options
Create Functions
create_*()
- Create a record with empty attributescreate_*(attrs)
- Create a record with given attributescreate_*!()
- Create a record with empty attributes (raises on error)create_*!(attrs)
- Create a record with given attributes (raises on error)
Get Functions
get_*(id)
- Get a record by IDget_*(id, opts)
- Get a record by ID with optionsget_*!(id)
- Get a record by ID (raises if not found)get_*!(id, opts)
- Get a record by ID with options (raises if not found)
Delete Functions
delete_*(entity)
- Delete a recorddelete_*!(entity)
- Delete a record (raises on error)
Change Functions
change_*(entity)
- Create a changeset from a recordchange_*(entity, attrs)
- Create a changeset from a record with attributes
Update Functions
update_*(entity)
- Update a recordupdate_*(entity, attrs)
- Update a record with attributes
New Functions
new_*()
- Initialize a new structnew_*(attrs)
- Initialize a new struct with attributesnew_*(attrs, opts)
- Initialize a new struct with attributes and options
Check the Functions guide for detailed documentation of each function.
Direct Core Usage
Sometimes you may want to use the core operations directly without generating context functions. This approach is particularly useful in scenarios such as:
- When working with dynamic schemas determined at runtime
- Building generic admin interfaces or APIs that handle multiple schemas
- Creating reusable modules that work with any schema
- Testing and prototyping before defining your final context structure
Here's how to use the core module directly:
alias Aurora.Ctx.Core
# List all records
products = Core.list(MyApp.Repo, MyApp.Inventory.Product)
# List with filtering and sorting
products = Core.list(MyApp.Repo, MyApp.Inventory.Product,
where: [status: :active],
order_by: [desc: :inserted_at],
preload: [:category]
)
# Paginated listing
page = Core.list_paginated(MyApp.Repo, MyApp.Inventory.Product,
paginate: %{page: 1, per_page: 20}
)
# Create a record
{:ok, product} = Core.create(MyApp.Repo, MyApp.Inventory.Product, %{
name: "Widget",
price: 19.99
})
# Update a record
{:ok, product} = Core.update(MyApp.Repo, product, %{price: 29.99})
# Delete a record
{:ok, _deleted} = Core.delete(MyApp.Repo, product)
Context vs Direct Core Usage
While using the Core module directly provides flexibility, using proper contexts has several advantages:
- Encapsulation: Contexts hide implementation details and provide a cleaner API
- Business Logic: Contexts are the ideal place to add business rules and validations
- Consistency: Generated functions ensure consistent naming and behavior across your app
- Documentation: Function names in contexts are more descriptive of their business purpose
- Maintainability: Changes to database operations can be centralized in contexts
- Type Safety: Generated functions include proper typespecs for better tooling support
Consider using proper contexts for most application code, and reserve direct Core usage for special cases where flexibility is more important than the benefits listed above.