ContextKit.CRUD.Scoped (ContextKit v0.5.0)
View SourceThe ContextKit.CRUD.Scoped
module provides a convenient way to generate standard CRUD (Create, Read, Update, Delete) operations for your Ecto schemas. It reduces boilerplate code by automatically generating commonly used database interaction functions.
Additionally, it supports scopes from Phoenix 1.8.
Setup
Add the following to your context module:
defmodule MyApp.Blog do
use ContextKit.CRUD.Scoped,
repo: MyApp.Repo,
schema: MyApp.Blog.Comment,
queries: MyApp.Blog.CommentQueries,
pubsub: MyApp.PubSub, # For realtime notifications via PubSub
scope: Application.compile_env(:my_app, :scopes)[:user], # To gain support for Phoenix 1.8 scopes.
except: [:delete], # Optional: exclude specific operations
plural_resource_name: "comments" # Optional: customize plural name
end
Required Options
:repo
- The Ecto repository module to use for database operations:schema
- The Ecto schema module that defines your resource:queries
- Module containing query-building functions for advanced filtering:pubsub
- The Phoenix.PubSub module to use for real-time features (required for subscription features):scope
- Configuration for scoping resources to specific contexts (e.g., user)
Optional Options
:except
- List of operation types to exclude (:new
,:list
,:get
,:one
,:delete
,:create
,:update
,:change
,:subscribe
,:broadcast
):plural_resource_name
- Custom plural name for list functions (defaults to singular + "s")
Generated Functions
For a schema named Comment
, the following functions are generated:
Query Operations
query_comments/0
- Returns a base query for all commentsquery_comments/1
- Returns a filtered query based on options (without executing)query_comments/2
- Returns a scoped and filtered query if:scope
is configured
New Operations
new_comment/0
- Returns a new commentnew_comment/1
- Returns a new comment with paramsnew_comment/2
- Returns a new comment with params and opts
List Operations
list_comments/0
- Returns all commentslist_comments/1
- Returns filtered comments based on optionslist_comments/2
- Returns scoped and filtered comments if:scope
is configured
Get Operations
get_comment/1
- Fetches a single comment by IDget_comment/2
- Fetches a comment by ID with additional filtersget_comment/3
- Fetches a scoped comment by ID with additional filters if:scope
is configuredget_comment!/1
- Likeget_comment/1
but raises if not foundget_comment!/2
- Likeget_comment/2
but raises if not foundget_comment!/3
- Likeget_comment/3
but raises if not found (with scope)
Single Record Operations
one_comment/1
- Fetches a single comment matching the criteriaone_comment/2
- Fetches a scoped single comment if:scope
is configuredone_comment!/1
- Likeone_comment/1
but raises if not foundone_comment!/2
- Likeone_comment/2
but raises if not found (with scope)
Save Operations
save_comment/1
- Saves (inserts or updates) a commentsave_comment/2
- Saves a comment with the provided attributessave_comment/3
- Saves a scoped comment with attributes if:scope
is configuredsave_comment!/1
- Likesave_comment/1
but raises on invalid attributessave_comment!/2
- Likesave_comment/2
but raises on invalid attributessave_comment!/3
- Likesave_comment/3
but raises on invalid attributes (with scope)
Create Operations
create_comment/1
- Creates a new comment with provided attributescreate_comment/2
- Creates a scoped comment if:scope
is configuredcreate_comment!/1
- Likecreate_comment/1
but raises on invalid attributescreate_comment!/2
- Likecreate_comment/2
but raises on invalid attributes (with scope)
Update Operations
update_comment/2
- Updates comment with provided attributesupdate_comment/3
- Updates scoped comment if:scope
is configuredupdate_comment!/2
- Likeupdate_comment/2
but raises on invalid attributesupdate_comment!/3
- Likeupdate_comment/3
but raises on invalid attributes (with scope)
Change Operations
change_comment/1
- Returns a changeset for the commentchange_comment/2
- Returns a changeset for the comment with changeschange_comment/3
- Returns a changeset for the scoped comment with changes if:scope
is configured
Delete Operations
delete_comment/1
- Deletes a comment struct or by query criteriadelete_comment/2
- Deletes a scoped comment if:scope
is configured
PubSub Operations (if :pubsub
and :scope
are configured)
subscribe_comments/1
- Subscribes to the scoped comments topicbroadcast_comment/2
- Broadcasts a message to the scoped comments topic
Query Options
All functions that accept options support:
- Basic filtering with field-value pairs
- Complex queries via
Ecto.Query
- Pagination via
paginate: true
orpaginate: [page: 1, per_page: 20]
- Custom query options defined in your queries module
- Scoping via
scope
when using scoped functions
Examples
# Get a query for comments (for use with Repo.aggregate, etc.)
query = MyApp.Blog.query_comments(status: :published)
MyApp.Repo.aggregate(query, :count)
# Get a query for comments scoped to current user
query = MyApp.Blog.query_comments(socket.assigns.current_scope)
MyApp.Repo.aggregate(query, :count)
# Get a query for comments scoped to current user with additional filters
query = MyApp.Blog.query_comments(socket.assigns.current_scope, status: :published)
MyApp.Repo.aggregate(query, :count)
# New comment
MyApp.Blog.new_comment()
# List all comments
MyApp.Blog.list_comments()
# List all comments belonging to current user
MyApp.Blog.list_comments(socket.assigns.current_scope)
# List published comments with pagination
MyApp.Blog.list_comments(status: :published, paginate: [page: 1])
# Get comment by ID with preloads
MyApp.Blog.get_comment(123, preload: [:user])
# Get comment by ID that belongs to current user
MyApp.Blog.get_comment(socket.assigns.current_scope, 123)
# Save a new comment (will be inserted)
MyApp.Blog.save_comment(%Comment{}, %{body: "Great post!", user_id: 42})
# Save an existing comment (will be updated)
MyApp.Blog.save_comment(existing_comment, %{body: "Updated content"})
# Save a comment with scope (will insert or update depending on the record's state)
MyApp.Blog.save_comment(socket.assigns.current_scope, comment, %{body: "Content"})
# Save a comment or raise on errors
MyApp.Blog.save_comment!(comment, %{body: "Content that must be saved"})
# Create a new comment (manually specifying user_id)
MyApp.Blog.create_comment(%{body: "Great post!", user_id: 42})
# Create a new comment that automatically belongs to current user
MyApp.Blog.create_comment(socket.assigns.current_scope, %{body: "Great post!"})
# Update a comment
MyApp.Blog.update_comment(comment, %{body: "Updated content"})
# Update a comment that belongs to current user
MyApp.Blog.update_comment(socket.assigns.current_scope, comment, %{body: "Updated content"})
# Get a changeset for updates
MyApp.Blog.change_comment(comment, %{body: "Changed content"})
# Get a changeset for updates with scope
MyApp.Blog.change_comment(socket.assigns.current_scope, comment, %{body: "Changed content"})
# Delete comment
MyApp.Blog.delete_comment(comment)
# Delete comment that belongs to current user
MyApp.Blog.delete_comment(socket.assigns.current_scope, comment)
# Delete comment matching criteria
MyApp.Blog.delete_comment(body: "Specific content to delete")
Each generated function can be overridden in your context module if you need custom behavior.
Queries Module
The required :queries
module should implement apply_query_option/2
, which receives a query option and the current query and returns a modified query. This allows for custom filtering, sorting, and other query modifications.
defmodule MyApp.Blog.CommentQueries do
import Ecto.Query
def apply_query_option({:with_user_name, name}, query) do
query
|> join(:inner, [c], u in assoc(c, :user))
|> where([_, u], ilike(u.name, ^"%#{name}%"))
end
def apply_query_option({:recent_first, true}, query) do
order_by(query, [c], desc: c.inserted_at)
end
def apply_query_option(_, query), do: query
end
Scope
A scope is a data structure used to keep information about the current request or session, such as the current user logged in, permissions, and so on. By using scopes, you have a single data structure that contains all relevant information, which is then passed around so all of your operations are properly scoped.
Usually you configure it with the same scope that you use in your Phoenix application:
scope: Application.compile_env(:my_app, :scopes)[:user],
pubsub: MyApp.PubSub # pubsub config is required for scopes
When a scope is configured, all relevant CRUD functions take an additional scope parameter as their first argument, ensuring that operations only affect records that belong to that scope.
For our comments example, the scope makes sure that comments are only listed, edited, or deleted by the user who created them.
Scope Configuration
If you pass in the scope with Application.compile_env(:my_app, :scopes)[:user]
- it will work automatically. You can also configure the scope manually. The scope configuration should be a keyword list with the following keys:
:module
- The module that defines the scope struct:access_path
- Path to access the scoping value (e.g.,[:user, :id]
for user ID):schema_key
- The field in the schema that corresponds to the scope (e.g.,:user_id
)
For our comments example, this would typically be:
scope: [
module: MyApp.Accounts.Scope,
access_path: [:user, :id],
schema_key: :user_id
]
Subscription Example
# Subscribe to comments created, updated, or deleted by the current user
MyApp.Blog.subscribe_comments(socket.assigns.current_scope)
# Now the current process will receive messages like:
# {:created, %Comment{}}
# {:updated, %Comment{}}
# {:deleted, %Comment{}}
Broadcasting Example
# Broadcast a custom message to all subscribers for current user's comments
MyApp.Blog.broadcast_comment(socket.assigns.current_scope, {:custom_event, comment})
Following messages are also broadcasted automatically for all create, update, delete operations:
{:created, %Comment{}}
{:updated, %Comment{}}
{:deleted, %Comment{}}