Skip to content

0.18.0

Choose a tag to compare

@igorbenav igorbenav released this 04 Nov 04:17
· 167 commits to main since this release
486e5c3

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_joined method
  • 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_prefix doesn'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

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

⚠️ None - This release maintains full backward compatibility with 0.17.x

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_name for 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

New Contributors

Full Changelog: v0.17.1...v0.18.0