0.18.0
0.18.0 Summary
Added
-
CountConfig for Related Object Counting by @doubledare704
- New configuration class for efficiently counting related objects in joined queries
- Supports many-to-many and one-to-many relationship counting via scalar subqueries
- Comprehensive support for composite primary keys and complex joins
- Seamless integration with
get_multi_joinedmethod
-
PaginatedRequestQuery Schema by @doubledare704
- Reusable Pydantic schema for standardizing pagination query parameters
- Supports both page-based and offset-based pagination modes
- Enhanced OpenAPI documentation with proper field descriptions
- Backward-compatible implementation with existing endpoints
-
Server-Side Field Injection (Pre-Processor Functions) by @LucasQR
- Automatic field injection for create, update, and delete operations
- CreateConfig, UpdateConfig, and DeleteConfig for comprehensive endpoint configuration
- Support for dependency injection with FastAPI's Depends system
- Security-focused field exclusion from API schemas and documentation
Fixed
- return_as_model Join Prefix Compatibility by @igorbenav
- Resolved silent failure when
join_prefixdoesn't match Pydantic schema field names - Added comprehensive validation with clear error messages for mismatched configurations
- Enhanced support for nested joins with proper field mapping
- Prevents data loss in joined relationships when using
return_as_model=True
- Resolved silent failure when
Improved
- Documentation and Community by @emiliano-gandini-outeda, @LucasQR, @igorbenav
- Added DeepWiki badge and documentation links to README
- Fixed Discord invite to use permanent link instead of temporary
- Enhanced join documentation with compatibility warnings and examples
- Community features and improved project accessibility
Breaking Changes
Details
CountConfig for Related Object Counting
Description
Introduced CountConfig to enable efficient counting of related objects without fetching the actual data. This is particularly useful for many-to-many relationships and scenarios where you need count information alongside your main query results.
Changes
- New CountConfig Class: Configurable counting for related objects in joined queries
- Scalar Subquery Implementation: Efficient counting that doesn't affect main query performance
- Composite Key Support: Full support for models with composite primary keys
- Flexible Aliasing: Custom aliases for count columns in results
Usage Examples
Basic Counting:
from fastcrud import FastCRUD, CountConfig
# Count participants for each project
project_crud = FastCRUD(Project)
count_config = CountConfig(
model=Participant,
join_on=(Participant.id == ProjectsParticipantsAssociation.participant_id)
& (ProjectsParticipantsAssociation.project_id == Project.id),
alias="participants_count",
)
result = await project_crud.get_multi_joined(
db=session,
counts_config=[count_config],
)Result:
{
"data": [
{"id": 1, "name": "Project Alpha", "participants_count": 3},
{"id": 2, "name": "Project Beta", "participants_count": 2},
{"id": 3, "name": "Project Gamma", "participants_count": 0}
],
"total_count": 3
}Counting with Filters:
# Count only developers for each project
developers_count = CountConfig(
model=Participant,
join_on=(Participant.id == ProjectsParticipantsAssociation.participant_id)
& (ProjectsParticipantsAssociation.project_id == Project.id),
alias="developers_count",
filters={"role": "Developer"},
)
result = await project_crud.get_multi_joined(
db=session,
counts_config=[developers_count],
)Multiple Count Configurations:
# Get different counts in a single query
all_participants = CountConfig(
model=Participant,
join_on=(Participant.id == ProjectsParticipantsAssociation.participant_id)
& (ProjectsParticipantsAssociation.project_id == Project.id),
alias="total_participants",
)
active_participants = CountConfig(
model=Participant,
join_on=(Participant.id == ProjectsParticipantsAssociation.participant_id)
& (ProjectsParticipantsAssociation.project_id == Project.id),
alias="active_participants",
filters={"status": "active"},
)
result = await project_crud.get_multi_joined(
db=session,
counts_config=[all_participants, active_participants],
)Benefits
- Performance Optimized: Uses scalar subqueries instead of multiple separate queries
- Flexible Configuration: Support for custom aliases and filtering conditions
- Relationship Agnostic: Works with one-to-many, many-to-many, and complex joins
- Composite Key Ready: Full support for models with composite primary keys
PaginatedRequestQuery Schema
Description
Introduced a standardized Pydantic schema for pagination query parameters that can be reused across different endpoints, improving consistency and reducing code duplication.
Changes
- Centralized Pagination Schema: Single source of truth for pagination parameters
- Enhanced Documentation: Proper OpenAPI field descriptions for better API docs
- Dual Mode Support: Both page-based and offset-based pagination in one schema
- Alias Support: Configured with
populate_by_namefor parameter flexibility
Implementation
from fastcrud.paginated.schemas import PaginatedRequestQuery
from fastapi import Depends
@app.get("/items")
async def get_items(
pagination: PaginatedRequestQuery = Depends(),
db: AsyncSession = Depends(get_session)
):
return await crud_items.get_multi(
db=db,
offset=pagination.offset,
limit=pagination.limit,
# ... other parameters
)Schema Structure
class PaginatedRequestQuery(BaseModel):
offset: int = Field(0, description="Number of items to skip")
limit: int = Field(100, description="Maximum number of items to return")
page: Optional[int] = Field(None, description="Page number (1-based)")
items_per_page: Optional[int] = Field(None, description="Items per page")
sort_columns: Optional[Union[str, List[str]]] = Field(None, description="Columns to sort by")
sort_orders: Optional[Union[str, List[str]]] = Field(None, description="Sort order (asc/desc)")Benefits
- Consistency: Standardized pagination across all endpoints
- Documentation: Better OpenAPI documentation with detailed field descriptions
- Flexibility: Support for different pagination patterns
- Maintainability: Centralized schema reduces code duplication
Server-Side Field Injection (Pre-Processor Functions)
Description
Introduced comprehensive server-side field injection capabilities that allow automatic population of fields during create, update, and delete operations. This feature enhances security and reduces boilerplate code.
Changes
- CreateConfig: Automatic field injection for create operations
- UpdateConfig: Field injection for update operations
- DeleteConfig: Pre-processing capabilities for delete operations
- Schema Modification: Dynamic exclusion of fields from API documentation
- Dependency Integration: Full support for FastAPI's dependency injection
Configuration Classes
CreateConfig:
from fastcrud import CreateConfig
from datetime import datetime
create_config = CreateConfig(
auto_fields={
"user_id": get_current_user_id, # From authentication
"created_by": get_current_user_id, # Audit field
"created_at": lambda: datetime.utcnow(), # Timestamp
"tenant_id": get_tenant_id, # Multi-tenancy
},
exclude_from_schema=["user_id", "created_by", "created_at", "tenant_id"]
)UpdateConfig:
update_config = UpdateConfig(
auto_fields={
"updated_by": get_current_user_id,
"updated_at": lambda: datetime.utcnow(),
"version": lambda: uuid4(), # Optimistic locking
},
exclude_from_schema=["updated_by", "updated_at", "version"]
)DeleteConfig:
delete_config = DeleteConfig(
auto_fields={
"deleted_by": get_current_user_id,
"deleted_at": lambda: datetime.utcnow(),
}
)Usage in CRUD Router
from fastcrud import crud_router
router = crud_router(
session=get_db,
model=Item,
create_schema=CreateItemSchema, # Excludes auto fields
update_schema=UpdateItemSchema, # Excludes auto fields
create_config=create_config,
update_config=update_config,
delete_config=delete_config,
)Security Benefits
- Field Protection: Prevents clients from setting sensitive fields
- Audit Trails: Automatic tracking of who created/modified records
- Multi-Tenancy: Automatic tenant isolation
- Timestamp Management: Consistent timestamp handling
- Authorization: Can include authorization checks in field functions
Use Cases
- User Scoped Data: Automatically filter data by current user
- Audit Trails: Track creation, modification, and deletion events
- Multi-Tenant Applications: Ensure data isolation between tenants
- Workflow Management: Set status fields based on business logic
- Financial Applications: Prevent tampering with calculated fields
- Content Moderation: Set review status on user-generated content
return_as_model Join Prefix Compatibility Fix
Description
Fixed a critical silent failure bug where return_as_model=True would lose joined data when join_prefix didn't match the expected Pydantic schema field names. The fix includes comprehensive validation with clear error messages.
Problem Solved
Previously, this configuration would silently fail:
# Schema expects "children" field
class ParentRead(BaseModel):
children: list[ChildRead] = []
# But join_prefix creates "child" key
join_config = JoinConfig(
join_prefix="child_", # Creates "child" key
relationship_type="one-to-many"
)
result = await crud.get_joined(
return_as_model=True, # Would silently return empty children
nest_joins=True,
joins_config=[join_config]
)
# Result: ParentRead(children=[]) # Data lost!Solution Implemented
- Validation Layer: Checks join_prefix compatibility with schema fields
- Clear Error Messages: Provides actionable guidance when mismatches occur
- Field Suggestions: Lists available schema fields for easy correction
- Documentation: Comprehensive examples and warnings in join documentation
Error Example
ValueError: join_prefix 'child_' creates key 'child' which is not a field in schema ParentRead.
Available fields: ['children', 'id', 'name'].
Either change join_prefix to match a schema field or use return_as_model=False.Correct Usage
# Matching configuration works correctly
join_config = JoinConfig(
join_prefix="children_", # Creates "children" key to match schema
relationship_type="one-to-many"
)
result = await crud.get_joined(
return_as_model=True,
nest_joins=True,
joins_config=[join_config]
)
# Result: ParentRead(children=[...actual children...])Benefits
- Data Integrity: Prevents silent data loss in joined relationships
- Developer Experience: Clear, actionable error messages
- Documentation: Enhanced join documentation with compatibility warnings
- Backward Compatibility: Existing working configurations continue to work
Documentation and Community Improvements
Description
Enhanced project documentation, community access, and developer experience through various improvements to README, documentation links, and community resources.
Changes
- DeepWiki Integration: Added badge and links to improve documentation discoverability
- Discord Community: Fixed invite link to use permanent invitation
- Enhanced Documentation: Added comprehensive warnings and examples for join compatibility
- Community Features: Improved project accessibility and community engagement
Documentation Enhancements
- Added clear warnings about join_prefix compatibility in advanced joins documentation
- Enhanced examples showing correct and incorrect usage patterns
- Improved error message documentation with specific examples
- Better structured documentation for new features
Community Improvements
- Fixed Discord server access with permanent invite links
- Added documentation badges for better visibility
- Enhanced README with better documentation links
- Improved project accessibility for new contributors
What's Changed
- add community by @igorbenav in #263
- Update README.md by @emiliano-gandini-outeda in #264
- Fixed the discord invite by @LucasQR in #265
- feat: Add CountConfig for counting related objects in get_multi_joined (#50) by @doubledare704 in #267
- feat: Add PaginatedRequestQuery schema for reusable query parameters (#258) by @doubledare704 in #266
- Pre-Processor Function by @LucasQR in #254
- Bug fix return as model by @igorbenav in #270
- change version to 0.18.0 by @igorbenav in #271
New Contributors
- @emiliano-gandini-outeda made their first contribution in #264
- @LucasQR made their first contribution in #265
Full Changelog: v0.17.1...v0.18.0