Skip to content

Commit

Permalink
Merge pull request #87 from igorbenav/endpoint-filters
Browse files Browse the repository at this point in the history
[WIP] Filters in Automatic Endpoints
  • Loading branch information
igorbenav authored May 21, 2024
2 parents adf9ab0 + 7bbe7db commit 03d4a2d
Show file tree
Hide file tree
Showing 15 changed files with 833 additions and 51 deletions.
96 changes: 96 additions & 0 deletions docs/advanced/endpoint.md
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,102 @@ This setup ensures that the soft delete functionality within your application ut

By specifying custom column names for soft deletion, you can adapt FastCRUD to fit the design of your database models, providing a flexible solution for handling deleted records in a way that best suits your application's needs.

## Using Filters in FastCRUD

FastCRUD provides filtering capabilities, allowing you to filter query results based on various conditions. Filters can be applied to `read_multi` and `read_paginated` endpoints. This section explains how to configure and use filters in FastCRUD.

### Defining Filters

Filters are either defined using the `FilterConfig` class or just passed as a dictionary. This class allows you to specify default filter values and validate filter types. Here's an example of how to define filters for a model:

```python
from fastcrud import FilterConfig

# Define filter configuration for a model
filter_config = FilterConfig(
tier_id=None, # Default filter value for tier_id
name=None # Default filter value for name
)
```

And the same thing using a `dict`:
```python
filter_config = {
"tier_id": None, # Default filter value for tier_id
"name": None, # Default filter value for name
}
```

By using `FilterConfig` you get better error messages.

### Applying Filters to Endpoints

You can apply filters to your endpoints by passing the `filter_config` to the `crud_router` or `EndpointCreator`. Here's an example:

```python
from fastcrud import crud_router
from yourapp.models import YourModel
from yourapp.schemas import CreateYourModelSchema, UpdateYourModelSchema
from yourapp.database import async_session

# Apply filters using crud_router
app.include_router(
crud_router(
session=async_session,
model=YourModel,
create_schema=CreateYourModelSchema,
update_schema=UpdateYourModelSchema,
filter_config=filter_config, # Apply the filter configuration
path="/yourmodel",
tags=["YourModel"]
)
)
```

### Using Filters in Requests

Once filters are configured, you can use them in your API requests. Filters are passed as query parameters. Here's an example of how to use filters in a request to a paginated endpoint:

```http
GET /yourmodel/get_paginated?page=1&itemsPerPage=3&tier_id=1&name=Alice
```

### Custom Filter Validation

The `FilterConfig` class includes a validator to check filter types. If an invalid filter type is provided, a `ValueError` is raised. You can customize the validation logic by extending the `FilterConfig` class:

```python
from fastcrud import FilterConfig
from pydantic import ValidationError

class CustomFilterConfig(FilterConfig):
@field_validator("filters")
def check_filter_types(cls, filters: dict[str, Any]) -> dict[str, Any]:
for key, value in filters.items():
if not isinstance(value, (type(None), str, int, float, bool)):
raise ValueError(f"Invalid default value for '{key}': {value}")
return filters

try:
# Example of invalid filter configuration
invalid_filter_config = CustomFilterConfig(invalid_field=[])
except ValidationError as e:
print(e)
```

### Handling Invalid Filter Columns

FastCRUD ensures that filters are applied only to valid columns in your model. If an invalid filter column is specified, a `ValueError` is raised:

```python
try:
# Example of invalid filter column
invalid_filter_config = FilterConfig(non_existent_column=None)
except ValueError as e:
print(e) # Output: Invalid filter column 'non_existent_column': not found in model
```


## Conclusion

The `EndpointCreator` class in FastCRUD offers flexibility and control over CRUD operations and custom endpoint creation. By extending this class or using the `included_methods` and `deleted_methods` parameters, you can tailor your API's functionality to your specific requirements, ensuring a more customizable and streamlined experience.
5 changes: 5 additions & 0 deletions docs/advanced/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,10 @@ Explore different ways of joining models in FastCRUD with examples and tips.

- [Joining Models](joins.md#applying-joins-in-fastcrud-methods)

### 8. Filters in Automatic Endpoints
Learn how to add query parameters to your `get_multi` and `get_paginated` endpoints.

- [Filters in Endpoints](endpoint.md#defining-filters)

## Prerequisites
Advanced usage assumes a solid understanding of the basic features and functionalities of our application. Knowledge of FastAPI, SQLAlchemy, and Pydantic is highly recommended to fully grasp the concepts discussed.
2 changes: 2 additions & 0 deletions fastcrud/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from .endpoint.endpoint_creator import EndpointCreator
from .endpoint.crud_router import crud_router
from .crud.helper import JoinConfig
from .endpoint.helper import FilterConfig

__all__ = [
"FastCRUD",
Expand All @@ -13,4 +14,5 @@
"JoinConfig",
"aliased",
"AliasedClass",
"FilterConfig",
]
3 changes: 2 additions & 1 deletion fastcrud/crud/fast_crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ def __init__(
updated_at_column: str = "updated_at",
) -> None:
self.model = model
self.model_col_names = [col.key for col in model.__table__.columns]
self.is_deleted_column = is_deleted_column
self.deleted_at_column = deleted_at_column
self.updated_at_column = updated_at_column
Expand Down Expand Up @@ -1690,7 +1691,7 @@ async def delete(
f"Expected exactly one record to delete, found {total_count}."
)

if self.is_deleted_column in self.model.__table__.columns:
if self.is_deleted_column in self.model_col_names:
update_stmt = (
update(self.model)
.filter(*filters)
Expand Down
58 changes: 58 additions & 0 deletions fastcrud/endpoint/crud_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from .endpoint_creator import EndpointCreator
from ..crud.fast_crud import FastCRUD
from .helper import FilterConfig

CreateSchemaType = TypeVar("CreateSchemaType", bound=BaseModel)
UpdateSchemaType = TypeVar("UpdateSchemaType", bound=BaseModel)
Expand Down Expand Up @@ -37,6 +38,7 @@ def crud_router(
deleted_at_column: str = "deleted_at",
updated_at_column: str = "updated_at",
endpoint_names: Optional[dict[str, str]] = None,
filter_config: Optional[Union[FilterConfig, dict]] = None,
) -> APIRouter:
"""
Creates and configures a FastAPI router with CRUD endpoints for a given model.
Expand Down Expand Up @@ -70,6 +72,7 @@ def crud_router(
endpoint_names: Optional dictionary to customize endpoint names for CRUD operations. Keys are operation types
("create", "read", "update", "delete", "db_delete", "read_multi", "read_paginated"), and
values are the custom names to use. Unspecified operations will use default names.
filter_config: Optional FilterConfig instance or dictionary to configure filters for the `read_multi` and `read_paginated` endpoints.
Returns:
Configured APIRouter instance with the CRUD endpoints.
Expand Down Expand Up @@ -229,6 +232,60 @@ async def add_routes_to_router(self, ...):
}
)
```
Using FilterConfig with dict:
```python
from fastapi import FastAPI
from fastcrud import crud_router
from myapp.models import MyModel
from myapp.schemas import CreateMyModel, UpdateMyModel
from myapp.database import async_session
app = FastAPI()
router = crud_router(
session=async_session,
model=MyModel,
create_schema=CreateMyModel,
update_schema=UpdateMyModel,
filter_config=FilterConfig(filters={"id": None, "name": "default"})
)
# Adds CRUD routes with filtering capabilities
app.include_router(router, prefix="/mymodel")
# Explanation:
# The FilterConfig specifies that 'id' should be a query parameter with no default value
# and 'name' should be a query parameter with a default value of 'default'.
# When fetching multiple items, you can filter by these parameters.
# Example GET request: /mymodel/get_multi?id=1&name=example
```
Using FilterConfig with keyword arguments:
```python
from fastapi import FastAPI
from fastcrud import crud_router
from myapp.models import MyModel
from myapp.schemas import CreateMyModel, UpdateMyModel
from myapp.database import async_session
app = FastAPI()
router = crud_router(
session=async_session,
model=MyModel,
create_schema=CreateMyModel,
update_schema=UpdateMyModel,
filter_config=FilterConfig(id=None, name="default")
)
# Adds CRUD routes with filtering capabilities
app.include_router(router, prefix="/mymodel")
# Explanation:
# The FilterConfig specifies that 'id' should be a query parameter with no default value
# and 'name' should be a query parameter with a default value of 'default'.
# When fetching multiple items, you can filter by these parameters.
# Example GET request: /mymodel/get_multi?id=1&name=example
```
"""
crud = crud or FastCRUD(
model=model,
Expand All @@ -252,6 +309,7 @@ async def add_routes_to_router(self, ...):
deleted_at_column=deleted_at_column,
updated_at_column=updated_at_column,
endpoint_names=endpoint_names,
filter_config=filter_config,
)

endpoint_creator_instance.add_routes_to_router(
Expand Down
Loading

0 comments on commit 03d4a2d

Please sign in to comment.