Repository: igorbenav/fastcrud
Pull request #204 (2025-02-23 09:40:59)

Implement advanced filter configs
Adds a new pytest fixture that creates a test client with predefined filter configurations (tier_id, name, name__startswith) to test filtered endpoints

# Pull Request Template for FastCRUD

## Description
I've added changes like author mentioned in https://github.com/igorbenav/fastcrud/issues/196

## Changes
Endpoint Creator now has extended logic in _validate_filter_config, but it waits for developer to add filters in FilterConfig, or like Simon did in generate_filter_configs.

## Tests
I've added test case to filter by startwith name, 1 positive case, 1 negative case.

## Checklist
- [x ] I have read the [CONTRIBUTING](CONTRIBUTING.md) document.
- [x ] My code follows the code style of this project.
- [ ] I have added necessary documentation (if appropriate).
- [x ] I have added tests that cover my changes (if applicable).
- [x ] All new and existing tests passed.

## Additional Notes
Include any additional information that you think is important for reviewers to know.



Patch:
diff --git a/fastcrud/endpoint/endpoint_creator.py b/fastcrud/endpoint/endpoint_creator.py
index f8156eb..ee92beb 100644
--- a/fastcrud/endpoint/endpoint_creator.py
+++ b/fastcrud/endpoint/endpoint_creator.py
@@ -323,8 +323,18 @@ class EndpointCreator:
 
     def _validate_filter_config(self, filter_config: FilterConfig) -> None:
         model_columns = self.crud.model_col_names
+        supported_filters = self.crud._SUPPORTED_FILTERS
         for key in filter_config.filters.keys():
-            if key not in model_columns:
+            if "__" in key:
+                field_name, op = key.rsplit("__", 1)
+                if op not in supported_filters:
+                    raise ValueError(
+                        f"Invalid filter op '{op}': following filter ops are allowed: {supported_filters.keys()}"
+                    )
+            else:
+                field_name = key
+
+            if field_name not in model_columns:
                 raise ValueError(
                     f"Invalid filter column '{key}': not found in model '{self.model.__name__}' columns"
                 )


Test patch:
diff --git a/tests/sqlalchemy/conftest.py b/tests/sqlalchemy/conftest.py
index b7743ac..68f87a9 100644
--- a/tests/sqlalchemy/conftest.py
+++ b/tests/sqlalchemy/conftest.py
@@ -571,7 +571,7 @@ def filtered_client(
             create_schema=create_schema,
             update_schema=update_schema,
             delete_schema=delete_schema,
-            filter_config=FilterConfig(tier_id=None, name=None),
+            filter_config=FilterConfig(tier_id=None, name=None, name__startswith=None),
             path="/test",
             tags=["test"],
             endpoint_names={
diff --git a/tests/sqlalchemy/crud/test_get_multi.py b/tests/sqlalchemy/crud/test_get_multi.py
index 9b5a837..3deb9f5 100644
--- a/tests/sqlalchemy/crud/test_get_multi.py
+++ b/tests/sqlalchemy/crud/test_get_multi.py
@@ -246,3 +246,29 @@ async def test_get_multi_handle_validation_error(async_session, test_model):
     assert "Data validation error for schema CustomCreateSchemaTest:" in str(
         exc_info.value
     )
+
+
+@pytest.mark.asyncio
+async def test_read_items_with_advanced_filters(
+        async_session, test_model, test_data
+):
+    for data in test_data:
+        new_item = test_model(**data)
+        async_session.add(new_item)
+    await async_session.commit()
+
+    crud = FastCRUD(test_model)
+
+    # Test startswith filter
+    name = "Ali"
+    result = await crud.get_multi(async_session, name__startswith=name)
+
+    assert len(result["data"]) > 0
+    for item in result["data"]:
+        assert item["name"].startswith(name)
+
+    # Test with non-matching filter
+    name = "Nothing"
+    result = await crud.get_multi(async_session, name__startswith=name)
+
+    assert len(result["data"]) == 0
diff --git a/tests/sqlmodel/conftest.py b/tests/sqlmodel/conftest.py
index 2c0a7a6..d1c4a8a 100644
--- a/tests/sqlmodel/conftest.py
+++ b/tests/sqlmodel/conftest.py
@@ -557,7 +557,7 @@ def filtered_client(
             create_schema=create_schema,
             update_schema=update_schema,
             delete_schema=delete_schema,
-            filter_config=FilterConfig(tier_id=None, name=None),
+            filter_config=FilterConfig(tier_id=None, name=None, name__startswith=None),
             path="/test",
             tags=["test"],
             endpoint_names={
diff --git a/tests/sqlmodel/endpoint/test_get_items.py b/tests/sqlmodel/endpoint/test_get_items.py
index eb42b52..77a37eb 100644
--- a/tests/sqlmodel/endpoint/test_get_items.py
+++ b/tests/sqlmodel/endpoint/test_get_items.py
@@ -116,3 +116,34 @@ async def test_read_items_with_schema(
     assert len(data["data"]) > 0
 
     assert all(read_schema.model_validate(item) for item in data["data"])
+
+
+@pytest.mark.asyncio
+async def test_read_items_with_advanced_filters(
+    filtered_client: TestClient, async_session, test_model, test_data
+):
+    for data in test_data:
+        new_item = test_model(**data)
+        async_session.add(new_item)
+    await async_session.commit()
+
+    name = "Ali"
+    response = filtered_client.get(f"/test/get_multi?name__startswith={name}")
+
+    assert response.status_code == 200
+    data = response.json()
+
+    assert "data" in data
+    assert len(data["data"]) > 0
+
+    for item in data["data"]:
+        assert item["name"].startswith(name)
+
+    name = "Nothing"
+    response = filtered_client.get(f"/test/get_multi?name__startswith={name}")
+
+    assert response.status_code == 200
+    data = response.json()
+
+    assert "data" in data
+    assert len(data["data"]) == 0