ContextKit (ContextKit v0.5.0)
View SourceContextKit provides a modular toolkit for building robust Phoenix/Ecto contexts with standardized CRUD operations.
Overview
ContextKit aims to reduce boilerplate code in Phoenix applications by providing:
- Standardized CRUD operation generators
- Dynamic query building with extensive filtering options
- Built-in pagination support
- Support for Phoenix 1.8 scopes
- PubSub integration for real-time updates
- Flexible and extensible design
Basic Usage
ContextKit offers two main modules:
ContextKit.CRUD
- Core CRUD operations with no scope supportContextKit.CRUD.Scoped
- CRUD operations with Phoenix 1.8 scope support and PubSub integration
Choose the module that fits your needs:
- Use
ContextKit.CRUD
for simple contexts like in Phoenix 1.7 without scopes or PubSub needs - Use
ContextKit.CRUD.Scoped
when working with Phoenix 1.8 scopes and wanting real-time updates
Getting Started
1. First, define your schema modules:
defmodule MyApp.Accounts.User do
use Ecto.Schema
import Ecto.Changeset
schema "users" do
field :email, :string
field :name, :string
field :status, :string
has_many :comments, MyApp.Blog.Comment
timestamps()
end
def changeset(user, attrs) do
user
|> cast(attrs, [:email, :name, :status])
|> validate_required([:email, :name])
|> unique_constraint(:email)
end
end
defmodule MyApp.Blog.Comment do
use Ecto.Schema
import Ecto.Changeset
schema "comments" do
field :body, :string
field :status, :string
belongs_to :user, MyApp.Accounts.User
timestamps()
end
# Standard changeset for unscoped operations
def changeset(comment, attrs) do
comment
|> cast(attrs, [:body, :status, :user_id])
|> validate_required([:body, :user_id])
end
# Scoped changeset for Phoenix 1.8 scope operations
def changeset(comment, attrs, scope) do
comment
|> cast(attrs, [:body, :status])
|> validate_required([:body])
|> put_change(:user_id, scope.user.id)
end
end
2. Create a queries module for custom query logic:
defmodule MyApp.Blog.CommentQueries do
import Ecto.Query
def apply_query_option({:with_recent_activity, true}, query) do
query
|> where([c], c.inserted_at > ago(1, "day"))
end
def apply_query_option({:with_user_name, name}, query) do
query
|> join(:inner, [c], u in assoc(c, :user))
|> where([_, u], u.name == ^name))
end
end
3. Use ContextKit.CRUD
for basic contexts:
# Basic usage without scopes
defmodule MyApp.Blog do
use ContextKit.CRUD,
repo: MyApp.Repo,
schema: MyApp.Blog.Comment,
queries: MyApp.Blog.CommentQueries
end
4. Or use ContextKit.CRUD.Scoped
for Phoenix 1.8 Scopes and PubSub:
defmodule MyApp.Blog do
use ContextKit.CRUD.Scoped,
repo: MyApp.Repo,
schema: MyApp.Blog.Comment,
queries: MyApp.Blog.CommentQueries,
pubsub: MyApp.PubSub,
scope: Application.compile_env(:my_app, :scopes)[:user]
end
You can define custom query functions directly in your context module by setting queries: __MODULE__
,
eliminating the need for a separate queries module. This is convenient for simpler contexts
where you don't need to share query logic.
Features
Standard CRUD Operations
ContextKit automatically generates common CRUD functions:
# Get query for use with Repo functions
Blog.query_comments()
Blog.query_comments(status: "published")
# New record
Blog.new_comment(%{status: "published"})
# List records with filtering and pagination
Blog.list_comments(status: "published", paginate: [page: 1, per_page: 20])
# Get single record
Blog.get_comment(123)
Blog.get_comment!(123) # Raises if not found
# Get one record by criteria
Blog.one_comment(user_id: 1)
Blog.one_comment!(user_id: 1) # Raises if not found
# Save records (insert or update based on record state)
Blog.save_comment(comment, %{body: "New or updated content"})
Blog.save_comment!(comment, %{body: "New or updated content"}) # Raises on error
# Create records
Blog.create_comment(%{body: "Great post!", user_id: 1})
Blog.create_comment!(%{body: "Great post!", user_id: 1}) # Raises on error
# Update records
Blog.update_comment(comment, %{body: "Updated comment"})
Blog.update_comment!(comment, %{body: "Updated comment"}) # Raises on error
# Get changeset
Blog.change_comment(comment)
Blog.change_comment(comment, %{body: "Changed comment"})
# Delete records
Blog.delete_comment(comment)
Blog.delete_comment(user_id: 1, body: "Specific comment")
Scoped Operations (Phoenix 1.8)
When using ContextKit.CRUD.Scoped
, all CRUD operations can take a scope parameter:
# Get query scoped to current user
Blog.query_comments(socket.assigns.current_scope)
Blog.query_comments(socket.assigns.current_scope, status: "published")
# List comments for the current user
Blog.list_comments(socket.assigns.current_scope)
# Get comment by ID only if it belongs to the current user
Blog.get_comment(socket.assigns.current_scope, 123)
# Save comment (insert or update) with scope
Blog.save_comment(socket.assigns.current_scope, comment, %{body: "New or updated content"})
# Create comment for the current user
Blog.create_comment(socket.assigns.current_scope, %{body: "Great post!"})
# Update comment only if it belongs to the current user
Blog.update_comment(socket.assigns.current_scope, comment, %{body: "Updated comment"})
# Delete comment only if it belongs to the current user
Blog.delete_comment(socket.assigns.current_scope, comment)
Advanced Filtering
Supports a wide range of filter operations:
# Basic equality
Blog.list_comments(status: "published")
# Complex filters
Blog.list_comments(filters: [
%{field: :body, op: :ilike, value: "%awesome%"},
%{field: :status, op: :in, value: ["published", "pending"]},
%{field: :inserted_at, op: :gt, value: ~N[2023-01-01 00:00:00]}
])
All fields from the schema can be filtered on automatically.
Any option not recognized as a field filter or standard query option is treated as a custom query option and passed to
the queries module's apply_query_option/2
function.
Query Operations
ContextKit provides query functions that return Ecto queries without executing them, useful for aggregation, composition, or further customization:
# Get a base query for all comments
query = Blog.query_comments()
# Apply filters to a query
query = Blog.query_comments(status: "published")
# Get a scoped query (with ContextKit.CRUD.Scoped)
query = Blog.query_comments(socket.assigns.current_scope)
# Use with Repo functions
MyApp.Repo.aggregate(query, :count)
MyApp.Repo.all(query)
# Compose with other queries
query
|> join(:left, [c], u in assoc(c, :user))
|> group_by([c, u], u.id)
|> select([c, u], {u.name, count(c.id)})
|> MyApp.Repo.all()
Query functions return an Ecto.Query
which is perfect for:
- Computing aggregations (count, sum, etc.)
- Creating complex reports with multiple joins
- Building sub-queries
- Performing custom operations that ContextKit's standard functions don't cover
Pagination
Built-in pagination support:
{comments, pagination} = Blog.list_comments(
status: "published",
paginate: [page: 2, per_page: 20]
)
# pagination struct includes:
# - total_count
# - total_pages
# - current_page
# - per_page
# - has_next_page?
# - has_previous_page?
# - next_page
# - previous_page
PubSub Integration
When using ContextKit.CRUD.Scoped
with pubsub
and scope
options, automatic broadcasting occurs for create, update, and delete operations:
# Subscribe to scoped updates
Blog.subscribe_comments(socket.assigns.current_scope)
# Now the current process will receive messages like:
# {:created, %Comment{}}
# {:updated, %Comment{}}
# {:deleted, %Comment{}}
# Broadcast custom messages
Blog.broadcast_comment(socket.assigns.current_scope, {:custom_event, comment})
Custom Query Options
Extend with custom query logic:
# In your queries module
def apply_query_option({:with_recent_activity, true}, query) do
query
|> where([c], c.inserted_at > ago(1, "day"))
end
# Usage
Blog.list_comments(with_recent_activity: true)
Configuration Options
When using ContextKit.CRUD
or ContextKit.CRUD.Scoped
, you can configure:
repo
: Your Ecto repository moduleschema
: The Ecto schema modulequeries
: Module containing custom query functionsexcept
: List of operations to exclude (:new
,:list
,:get
,:one
,:delete
,:create
,:update
,:change
,:subscribe
,:broadcast
)plural_resource_name
: Custom plural name for list functions
Additional options for ContextKit.CRUD.Scoped
:
pubsub
: Phoenix.PubSub module for real-time featuresscope
: Configuration for Phoenix 1.8 scopes
Best Practices
- Choose the right module:
ContextKit.CRUD
for simple contexts,ContextKit.CRUD.Scoped
for contexts with scopes - Create separate query modules for complex filtering logic
- Override generated functions when you need custom behavior
- Use pagination for large datasets
- When using scopes, implement schema-level changeset functions that accept a scope parameter
- Use PubSub for real-time updates in LiveView applications