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.3"}
]
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 this overview:
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
Aurora.Ctx respects existing function definitions. You can provide custom implementations for specific operations while letting Aurora.Ctx generate the rest. Functions can be defined anywhere in the module:
defmodule MyApp.Inventory do
use Aurora.Ctx
ctx_register_schema(MyApp.Inventory.Product)
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
end
Changeset Customization
Aurora.Ctx provides several options for customizing which changeset functions are used:
defmodule MyApp.Inventory do
use Aurora.Ctx
# Default changeset for all operations
ctx_register_schema(Product, changeset: :custom_changeset)
# Separate changesets for create/update
ctx_register_schema(Product,
create_changeset: :creation_changeset,
update_changeset: &MyCustomProductChangeset.update_changeset/2
)
end
The priority for selecting changeset functions is:
- Operation-specific changeset (create_changeset/update_changeset)
- Default changeset specified with :changeset option
- Standard "changeset/2" function in the schema
Generated Functions
For each schema, the following functions are automatically generated:
List Functions
list_*(opts \\ nil)
- List all records with optional filtering/sortinglist_*_paginated(opts \\ %{})
- List records with pagination and optionscount_*(opts \\ nil)
- Count records with optional filtering
Pagination Functions
to_*_page(pagination, page_number)
- Navigates to the specified page numbernext_*_page(pagination)
- Advances to the next page of resultsprevious_*_page(pagination)
- Goes back to the previous page of results
Create Functions
create_*(attrs \\ %{})
- Create a record with optional attributescreate_*!(attrs \\ %{})
- Create a record with optional attributes (raises on error)
Get Functions
get_*(id, opts \\ [])
- Get a record by ID with optional preloadsget_*!(id, opts \\ [])
- Get a record by ID with optional preloads (raises if not found)get_*_by(clauses, opts \\ [])
- Get a record by clauses and query options (raises if found more than one)get_*_by!(clauses), opts \\ [])
- Get a record by clauses and query options (raises if not found, or if more than one)
Delete Functions
delete_*(entity)
- Delete the given recorddelete_*!(entity)
- Delete the given record (raises on error)
Change Functions
change_*(entity_or_changeset, attrs \\ %{})
- Create a changeset from an entity or existing changeset with optional attributes
Update Functions
update_*(entity_or_changeset)
- Update a record from an entity or changesetupdate_*(entity_or_changeset, attrs)
- Update a record with attributes, accepts entity or changesetupdate_*!(entity_or_changeset)
- Update a record from an entity or changeset (raises on error)update_*!(entity_or_changeset, attrs)
- Update a record with attributes, accepts entity or changeset (raises on error)
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
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.