fastapi-basekit 0.5.2__tar.gz → 0.5.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/PKG-INFO +1 -1
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/aio/beanie/repository/base.py +113 -71
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/aio/beanie/service/base.py +39 -3
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/aio/controller/base.py +27 -7
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/aio/sqlalchemy/repository/base.py +34 -9
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/aio/sqlalchemy/service/base.py +10 -3
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/exceptions/handler.py +67 -18
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/utils/exception_handlers.py +27 -11
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit.egg-info/PKG-INFO +1 -1
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit.egg-info/SOURCES.txt +2 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/pyproject.toml +1 -1
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/tests/test_beanie_service_regression.py +73 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/tests/test_exception_handlers.py +17 -0
- fastapi_basekit-0.5.4/tests/test_format_response_schema_override.py +79 -0
- fastapi_basekit-0.5.4/tests/test_owner_filter_helper.py +65 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/tests/test_sqlalchemy_service_regression.py +73 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/LICENSE +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/README.md +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/__init__.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/aio/__init__.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/aio/beanie/__init__.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/aio/beanie/controller/__init__.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/aio/beanie/controller/base.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/aio/beanie/repository/__init__.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/aio/beanie/service/__init__.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/aio/controller/__init__.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/aio/permissions/__init__.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/aio/permissions/base.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/aio/sqlalchemy/__init__.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/aio/sqlalchemy/controller/__init__.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/aio/sqlalchemy/controller/base.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/aio/sqlalchemy/repository/__init__.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/aio/sqlalchemy/service/__init__.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/aio/sqlalchemy/session.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/aio/sqlmodel/__init__.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/aio/sqlmodel/controller/__init__.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/aio/sqlmodel/controller/base.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/aio/sqlmodel/repository/__init__.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/aio/sqlmodel/repository/base.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/aio/sqlmodel/service/__init__.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/aio/sqlmodel/service/base.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/cli/__init__.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/cli/main.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/exceptions/__init__.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/exceptions/api_exceptions.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/exceptions/domain.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/openapi.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/py.typed +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/schema/__init__.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/schema/base.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/schema/jwt.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/schema/schema.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/servicios/__init__.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/servicios/thrid/__init__.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/servicios/thrid/jwt.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/cookiecutter.json +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/hooks/post_gen_project.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/hooks/pre_gen_project.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/.env.example +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/.gitignore +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/Dockerfile +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/LICENSE +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/Makefile +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/README.md +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/alembic/env.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/alembic/script.py.mako +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/alembic.ini +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/__init__.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/__init__.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/v1/__init__.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/v1/endpoints/__init__.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/v1/endpoints/auth/__init__.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/v1/endpoints/auth/auth.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/v1/endpoints/user/__init__.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/v1/endpoints/user/user.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/v1/routers.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/config/__init__.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/config/database.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/config/settings.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/main.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/middleware/__init__.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/middleware/auth.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/middleware/permissions.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/models/__init__.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/models/auth.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/models/base.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/models/enums.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/models/types.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/permissions/__init__.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/permissions/user.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/repositories/__init__.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/repositories/user/__init__.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/repositories/user/repository.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/schemas/__init__.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/schemas/auth.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/schemas/base.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/schemas/user.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/scripts/__init__.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/scripts/init.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/scripts/init_admin.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/services/__init__.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/services/auth_service.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/services/dependency.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/services/user_service.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/utils/__init__.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/utils/security.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/docker-compose.yml +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/pytest.ini +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/requirements.txt +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit.egg-info/dependency_links.txt +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit.egg-info/entry_points.txt +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit.egg-info/requires.txt +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit.egg-info/top_level.txt +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/setup.cfg +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/tests/test_action_param_leak.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/tests/test_api_exceptions.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/tests/test_base_response.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/tests/test_base_service.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/tests/test_beanie_aggregation_hooks.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/tests/test_beanie_aggregation_integration.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/tests/test_beanie_keyset_pagination.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/tests/test_beanie_repository_regression.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/tests/test_beanie_service_mutable_defaults.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/tests/test_controller_auto_permissions.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/tests/test_crud_beanie_controller.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/tests/test_crud_controller.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/tests/test_crud_sqlmodel_repository_service.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/tests/test_filter_operators.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/tests/test_generics_optional_and_cache.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/tests/test_generics_wiring.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/tests/test_jwt_config_robustness.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/tests/test_jwt_service.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/tests/test_openapi_simplify.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/tests/test_params_classification.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/tests/test_params_coercion_matrix.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/tests/test_params_coercion_unit.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/tests/test_params_http_matrix_beanie.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/tests/test_params_http_matrix_sqlalchemy.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/tests/test_params_rich_type_coercion.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/tests/test_path_filter_mixin_compat.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/tests/test_post_process_list_hook.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/tests/test_pulbot_cbv_pattern_beanie.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/tests/test_repo_signature_aliases.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/tests/test_sql_queryset_override.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/tests/test_sqlalchemy_base_service_order.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/tests/test_sqlalchemy_repository_regression.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/tests/test_sqlmodel_filter_operators.py +0 -0
- {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/tests/test_typed_filter_coercion.py +0 -0
{fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/aio/beanie/repository/base.py
RENAMED
|
@@ -119,75 +119,60 @@ class BaseRepository(Generic[ModelT]):
|
|
|
119
119
|
kwargs["projection"] = projection
|
|
120
120
|
return kwargs
|
|
121
121
|
|
|
122
|
-
def
|
|
123
|
-
|
|
124
|
-
search: Optional[str],
|
|
125
|
-
search_fields: List[str],
|
|
126
|
-
filters: dict = None,
|
|
127
|
-
order_by: Optional[List[tuple]] = None,
|
|
128
|
-
**kwargs,
|
|
129
|
-
) -> FindMany[Document]:
|
|
130
|
-
"""Versión personalizada que soporta campos Link."""
|
|
131
|
-
exprs = []
|
|
132
|
-
|
|
133
|
-
if search and search_fields:
|
|
134
|
-
exprs.append(
|
|
135
|
-
Or(
|
|
136
|
-
*[
|
|
137
|
-
RegEx(
|
|
138
|
-
getattr(self.model, f),
|
|
139
|
-
f".*{search}.*",
|
|
140
|
-
options="i",
|
|
141
|
-
)
|
|
142
|
-
for f in search_fields
|
|
143
|
-
]
|
|
144
|
-
)
|
|
145
|
-
)
|
|
146
|
-
|
|
147
|
-
# Obtener campos del modelo
|
|
122
|
+
def _is_link_field(self, field_name: str) -> bool:
|
|
123
|
+
"""Verifica si un campo del modelo es de tipo Link (o Optional[Link])."""
|
|
148
124
|
model_fields = (
|
|
149
125
|
self.model.model_fields
|
|
150
126
|
if hasattr(self.model, "model_fields")
|
|
151
127
|
else {}
|
|
152
128
|
)
|
|
129
|
+
field_info = model_fields.get(field_name)
|
|
130
|
+
if not field_info:
|
|
131
|
+
return False
|
|
153
132
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
field_info = model_fields.get(field_name)
|
|
157
|
-
if not field_info:
|
|
158
|
-
return False
|
|
159
|
-
|
|
160
|
-
field_type = field_info.annotation
|
|
161
|
-
origin = get_origin(field_type)
|
|
162
|
-
|
|
163
|
-
# Caso directo: Link[Model]
|
|
164
|
-
if origin is Link:
|
|
165
|
-
return True
|
|
166
|
-
|
|
167
|
-
# Caso Optional[Link[Model]] = Union[Link[Model], None]
|
|
168
|
-
# O cualquier Union que contenga Link
|
|
169
|
-
if origin is not None:
|
|
170
|
-
args = get_args(field_type)
|
|
171
|
-
for arg in args:
|
|
172
|
-
# Verificar si el argumento es Link
|
|
173
|
-
arg_origin = get_origin(arg)
|
|
174
|
-
if arg_origin is Link:
|
|
175
|
-
return True
|
|
133
|
+
field_type = field_info.annotation
|
|
134
|
+
origin = get_origin(field_type)
|
|
176
135
|
|
|
177
|
-
|
|
136
|
+
# Caso directo: Link[Model]
|
|
137
|
+
if origin is Link:
|
|
138
|
+
return True
|
|
178
139
|
|
|
179
|
-
|
|
140
|
+
# Caso Optional[Link[Model]] = Union[Link[Model], None]
|
|
141
|
+
# O cualquier Union que contenga Link
|
|
142
|
+
if origin is not None:
|
|
143
|
+
args = get_args(field_type)
|
|
144
|
+
for arg in args:
|
|
145
|
+
arg_origin = get_origin(arg)
|
|
146
|
+
if arg_origin is Link:
|
|
147
|
+
return True
|
|
180
148
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
return ObjectId(value)
|
|
188
|
-
except Exception:
|
|
189
|
-
return value
|
|
149
|
+
return False
|
|
150
|
+
|
|
151
|
+
@staticmethod
|
|
152
|
+
def _coerce_objectid(value: Any) -> Any:
|
|
153
|
+
"""str/UUID → ObjectId para queries Mongo sobre Link.id."""
|
|
154
|
+
if isinstance(value, ObjectId):
|
|
190
155
|
return value
|
|
156
|
+
if isinstance(value, str):
|
|
157
|
+
try:
|
|
158
|
+
return ObjectId(value)
|
|
159
|
+
except Exception:
|
|
160
|
+
return value
|
|
161
|
+
return value
|
|
162
|
+
|
|
163
|
+
def _resolve_filter_query_args(self, filters: dict = None) -> list:
|
|
164
|
+
"""Traduce un dict de `get_filters()` a los args posicionales que
|
|
165
|
+
acepta `Model.find()`/`Model.find_one()` — resuelve campos Link
|
|
166
|
+
(directos y el alias `<field>_id`), pasa claves Mongo crudas
|
|
167
|
+
(`"user.$id"`, `"$or"`, ...) como dict, y avisa (no falla en
|
|
168
|
+
silencio) si una clave no resuelve a nada del modelo.
|
|
169
|
+
|
|
170
|
+
Compartido por `build_filter_query` (listados) y `get_by_id`
|
|
171
|
+
(retrieve/update/delete escopeados) — mismo motor de resolución de
|
|
172
|
+
filtros para las dos rutas, así un `get_filters()` de seguridad
|
|
173
|
+
protege ambas por igual."""
|
|
174
|
+
exprs: list = []
|
|
175
|
+
raw_filters: Dict[str, Any] = {}
|
|
191
176
|
|
|
192
177
|
for k, v in (filters or {}).items():
|
|
193
178
|
# MongoDB-style keys (dot-notation like "user.$id" or operators like "$or")
|
|
@@ -198,8 +183,8 @@ class BaseRepository(Generic[ModelT]):
|
|
|
198
183
|
|
|
199
184
|
if hasattr(self.model, k):
|
|
200
185
|
field_attr = getattr(self.model, k)
|
|
201
|
-
if _is_link_field(k):
|
|
202
|
-
exprs.append(field_attr.id == _coerce_objectid(v))
|
|
186
|
+
if self._is_link_field(k):
|
|
187
|
+
exprs.append(field_attr.id == self._coerce_objectid(v))
|
|
203
188
|
else:
|
|
204
189
|
exprs.append(field_attr == v)
|
|
205
190
|
continue
|
|
@@ -210,32 +195,59 @@ class BaseRepository(Generic[ModelT]):
|
|
|
210
195
|
# el modelo y es un Link[X].
|
|
211
196
|
if k.endswith("_id"):
|
|
212
197
|
base = k[:-3]
|
|
213
|
-
if hasattr(self.model, base) and _is_link_field(base):
|
|
198
|
+
if hasattr(self.model, base) and self._is_link_field(base):
|
|
214
199
|
field_attr = getattr(self.model, base)
|
|
215
|
-
exprs.append(field_attr.id == _coerce_objectid(v))
|
|
200
|
+
exprs.append(field_attr.id == self._coerce_objectid(v))
|
|
216
201
|
continue
|
|
217
202
|
|
|
218
203
|
# Clave no resoluble (typo, campo inexistente, nesting mal escrito).
|
|
219
204
|
# Se descarta — pero se AVISA: si la clave venía de `get_filters`
|
|
220
205
|
# (scoping por tenant/owner), su desaparición silenciosa deja el
|
|
221
|
-
# listado
|
|
222
|
-
#
|
|
206
|
+
# listado/retrieve SIN ese filtro. El warning hace visible la
|
|
207
|
+
# pérdida en logs/Sentry en vez de fallar en silencio.
|
|
223
208
|
logger.warning(
|
|
224
|
-
"
|
|
225
|
-
"ningún campo de %s). Si era un filtro de
|
|
226
|
-
"queda SIN ese filtro.",
|
|
209
|
+
"_resolve_filter_query_args: filtro '%s' descartado (no "
|
|
210
|
+
"resuelve a ningún campo de %s). Si era un filtro de "
|
|
211
|
+
"scoping, la query queda SIN ese filtro.",
|
|
227
212
|
k,
|
|
228
213
|
getattr(self.model, "__name__", self.model),
|
|
229
214
|
)
|
|
230
215
|
|
|
231
216
|
# Raw MongoDB filters go first so Beanie processes them as a dict condition
|
|
232
|
-
|
|
217
|
+
return ([raw_filters] if raw_filters else []) + exprs
|
|
218
|
+
|
|
219
|
+
def build_filter_query(
|
|
220
|
+
self,
|
|
221
|
+
search: Optional[str],
|
|
222
|
+
search_fields: List[str],
|
|
223
|
+
filters: dict = None,
|
|
224
|
+
order_by: Optional[List[tuple]] = None,
|
|
225
|
+
**kwargs,
|
|
226
|
+
) -> FindMany[Document]:
|
|
227
|
+
"""Versión personalizada que soporta campos Link."""
|
|
228
|
+
exprs = []
|
|
229
|
+
|
|
230
|
+
if search and search_fields:
|
|
231
|
+
exprs.append(
|
|
232
|
+
Or(
|
|
233
|
+
*[
|
|
234
|
+
RegEx(
|
|
235
|
+
getattr(self.model, f),
|
|
236
|
+
f".*{search}.*",
|
|
237
|
+
options="i",
|
|
238
|
+
)
|
|
239
|
+
for f in search_fields
|
|
240
|
+
]
|
|
241
|
+
)
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
query_args: list = exprs + self._resolve_filter_query_args(filters)
|
|
233
245
|
query = self.model.find(*query_args, **self._get_query_kwargs(**kwargs))
|
|
234
|
-
|
|
246
|
+
|
|
235
247
|
# Apply ordering if provided
|
|
236
248
|
if order_by:
|
|
237
249
|
query = query.sort(order_by)
|
|
238
|
-
|
|
250
|
+
|
|
239
251
|
return query
|
|
240
252
|
|
|
241
253
|
async def paginate(
|
|
@@ -521,10 +533,40 @@ class BaseRepository(Generic[ModelT]):
|
|
|
521
533
|
async def get_by_id(
|
|
522
534
|
self,
|
|
523
535
|
obj_id: Union[str, ObjectId],
|
|
536
|
+
filters: Optional[Dict[str, Any]] = None,
|
|
524
537
|
**kwargs,
|
|
525
538
|
) -> Optional[ModelT]:
|
|
539
|
+
"""`filters` (opcional) — scoping de seguridad (típicamente
|
|
540
|
+
`Service.get_filters()`, ej. `{"user.$id": request.state.user.id}`).
|
|
541
|
+
Sin `filters`, comportamiento idéntico a antes (busca por id, sin
|
|
542
|
+
scope) — 100% retrocompatible. Con `filters`, un id que existe pero
|
|
543
|
+
no matchea el scope devuelve `None` (mismo resultado que "no
|
|
544
|
+
encontrado"), cerrando la clase de IDOR de "conozco el id de otro
|
|
545
|
+
tenant y lo pido directo".
|
|
546
|
+
|
|
547
|
+
El chequeo de scope corre SIEMPRE con `fetch_links=False` (find_one
|
|
548
|
+
plano, sobre el documento crudo — DBRef sin resolver), sea cual sea
|
|
549
|
+
el `fetch_links` que pida el caller para el fetch final. Motivo:
|
|
550
|
+
Beanie reescribe `find(..., fetch_links=True)` a una aggregation con
|
|
551
|
+
`$lookup`, y un filtro con notación de punto (`"user.$id"`,
|
|
552
|
+
`"user._id"`, etc.) puede matchear distinto — o no matchear nada —
|
|
553
|
+
según si corre ANTES o DESPUÉS del `$lookup` (mismo bug ya
|
|
554
|
+
documentado en `find_all_production_for_user` de pulbot-backend:
|
|
555
|
+
"fetch_links=True puede descartar resultados silenciosamente al
|
|
556
|
+
filtrar por campos dict anidados"). Combinar scope-filter +
|
|
557
|
+
fetch_links en una sola query es frágil; separarlos lo hace
|
|
558
|
+
determinístico sin importar qué `get_filters()` use cada service."""
|
|
526
559
|
if not isinstance(obj_id, ObjectId):
|
|
527
560
|
obj_id = ObjectId(obj_id)
|
|
561
|
+
|
|
562
|
+
if filters:
|
|
563
|
+
scope_exprs = self._resolve_filter_query_args(filters)
|
|
564
|
+
scoped = await self.model.find_one(
|
|
565
|
+
self.model.id == obj_id, *scope_exprs, fetch_links=False
|
|
566
|
+
)
|
|
567
|
+
if not scoped:
|
|
568
|
+
return None
|
|
569
|
+
|
|
528
570
|
return await self.model.find_one(
|
|
529
571
|
self.model.id == obj_id,
|
|
530
572
|
**self._get_query_kwargs(**kwargs),
|
|
@@ -82,9 +82,43 @@ class BaseService(Generic[ModelT]):
|
|
|
82
82
|
filters = filters or {}
|
|
83
83
|
return filters
|
|
84
84
|
|
|
85
|
+
def owner_filter(self, field: str, value: Any) -> Dict[str, Any]:
|
|
86
|
+
"""Filtro de scoping por un campo `Link` (ej. ownership por
|
|
87
|
+
`user`), correcto tanto en `list()` como en `retrieve`/`update`/
|
|
88
|
+
`delete` — sin que el service tenga que conocer por qué la clave
|
|
89
|
+
Mongo cambia entre esos dos caminos.
|
|
90
|
+
|
|
91
|
+
Por qué existen dos claves para "lo mismo": un campo `Link[User]`
|
|
92
|
+
se guarda como DBRef (`{"$ref": "users", "$id": ObjectId(...)}`).
|
|
93
|
+
- `retrieve`/`update`/`delete` van por `get_by_id`, que SIEMPRE
|
|
94
|
+
resuelve el scope con `fetch_links=False` (`find_one` plano sobre
|
|
95
|
+
el documento crudo) — ahí el DBRef está intacto, la clave correcta
|
|
96
|
+
es `"<field>.$id"`.
|
|
97
|
+
- `list()` (y cualquier action cuyo `get_kwargs_query()` pida
|
|
98
|
+
`fetch_links=True`) corre `Model.find(..., fetch_links=True)`,
|
|
99
|
+
que Beanie reescribe a una aggregation con `$lookup` — ahí el
|
|
100
|
+
`$lookup` REEMPLAZA el DBRef por el subdocumento resuelto, así
|
|
101
|
+
que la clave correcta pasa a ser `"<field>._id"`.
|
|
102
|
+
Usar la clave equivocada no tira error — devuelve 0 resultados en
|
|
103
|
+
silencio (bug real ya encontrado usando esto a mano en
|
|
104
|
+
`ToolService` de pulbot-backend, 2026-08-11). Este helper decide
|
|
105
|
+
por vos: usalo en vez de escribir `"<field>.$id"`/`"<field>._id"`
|
|
106
|
+
directo en tu `get_filters()`.
|
|
107
|
+
|
|
108
|
+
⚠️ Asume que tu action de lectura-por-id (`retrieve`/`update`/
|
|
109
|
+
`delete`) no fuerza `fetch_links=True` en el PROPIO chequeo de
|
|
110
|
+
scope — así es como quedó `get_by_id` del repo (siempre
|
|
111
|
+
`fetch_links=False` para el scope-check, independientemente del
|
|
112
|
+
`fetch_links` que pidas para el fetch final). Si tu repo está
|
|
113
|
+
parcheado para saltarse eso, este helper no aplica."""
|
|
114
|
+
suffix = "._id" if self.action == "list" else ".$id"
|
|
115
|
+
return {f"{field}{suffix}": value}
|
|
116
|
+
|
|
85
117
|
async def retrieve(self, id: str) -> ModelT:
|
|
86
118
|
kwargs = self.get_kwargs_query()
|
|
87
|
-
obj = await self.repository.get_by_id(
|
|
119
|
+
obj = await self.repository.get_by_id(
|
|
120
|
+
id, filters=self.get_filters(), **kwargs
|
|
121
|
+
)
|
|
88
122
|
if not obj:
|
|
89
123
|
raise NotFoundException(f"id={id} no encontrado")
|
|
90
124
|
return obj
|
|
@@ -241,7 +275,9 @@ class BaseService(Generic[ModelT]):
|
|
|
241
275
|
|
|
242
276
|
async def update(self, id: str, data: BaseModel) -> ModelT:
|
|
243
277
|
kwargs = self.get_kwargs_query()
|
|
244
|
-
obj = await self.repository.get_by_id(
|
|
278
|
+
obj = await self.repository.get_by_id(
|
|
279
|
+
id, filters=self.get_filters(), **kwargs
|
|
280
|
+
)
|
|
245
281
|
if not obj:
|
|
246
282
|
raise NotFoundException(f"id={id} no encontrado")
|
|
247
283
|
if isinstance(data, BaseModel):
|
|
@@ -250,7 +286,7 @@ class BaseService(Generic[ModelT]):
|
|
|
250
286
|
return updated
|
|
251
287
|
|
|
252
288
|
async def delete(self, id: str) -> str:
|
|
253
|
-
obj = await self.repository.get_by_id(id)
|
|
289
|
+
obj = await self.repository.get_by_id(id, filters=self.get_filters())
|
|
254
290
|
if not obj:
|
|
255
291
|
raise NotFoundException(f"id={id} no encontrado")
|
|
256
292
|
await self.repository.delete(obj)
|
|
@@ -264,13 +264,33 @@ class BaseController:
|
|
|
264
264
|
# raw value when the schema doesn't fit (custom-action endpoints
|
|
265
265
|
# often return ad-hoc dicts that don't match the controller's
|
|
266
266
|
# default schema_class — those should pass through untouched).
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
267
|
+
#
|
|
268
|
+
# Cuando un endpoint custom ya devuelve un modelo Pydantic específico
|
|
269
|
+
# (distinto a ``schema``), no lo re-casteeamos al schema por defecto:
|
|
270
|
+
# eso rompe el ``response_model`` declarado en la ruta y provoca
|
|
271
|
+
# ResponseValidationError. Se convierte a dict limpio y FastAPI se
|
|
272
|
+
# encarga de validar/serializar contra el response_model del endpoint.
|
|
273
|
+
if isinstance(data, BaseModel):
|
|
274
|
+
if isinstance(data, schema):
|
|
275
|
+
data_parsed = data
|
|
276
|
+
else:
|
|
277
|
+
data_parsed = self.to_dict(data)
|
|
278
|
+
|
|
279
|
+
elif isinstance(data, list):
|
|
280
|
+
# Si la lista contiene modelos Pydantic distintos al schema por
|
|
281
|
+
# defecto, respetarlos; de lo contrario validar normalmente.
|
|
282
|
+
if data and all(
|
|
283
|
+
isinstance(item, BaseModel) and not isinstance(item, schema)
|
|
284
|
+
for item in data
|
|
285
|
+
):
|
|
286
|
+
data_parsed = [self.to_dict(item) for item in data]
|
|
287
|
+
else:
|
|
288
|
+
data_dicts = [self.to_dict(item) for item in data]
|
|
289
|
+
try:
|
|
290
|
+
adapter = TypeAdapter(List[schema])
|
|
291
|
+
data_parsed = adapter.validate_python(data_dicts)
|
|
292
|
+
except Exception:
|
|
293
|
+
data_parsed = data_dicts
|
|
274
294
|
|
|
275
295
|
elif isinstance(data, dict):
|
|
276
296
|
try:
|
{fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/aio/sqlalchemy/repository/base.py
RENAMED
|
@@ -487,16 +487,37 @@ class BaseRepository(Generic[ModelT]):
|
|
|
487
487
|
|
|
488
488
|
return record
|
|
489
489
|
|
|
490
|
-
async def get(
|
|
491
|
-
|
|
490
|
+
async def get(
|
|
491
|
+
self,
|
|
492
|
+
record_id: Union[str, UUID],
|
|
493
|
+
filters: Optional[Dict[str, Any]] = None,
|
|
494
|
+
) -> Optional[ModelT]:
|
|
495
|
+
"""Obtiene un registro por su ID.
|
|
496
|
+
|
|
497
|
+
`filters` (opcional) — scoping de seguridad (típicamente
|
|
498
|
+
`Service.get_filters()`). Sin `filters`, usa `session.get()` (fast
|
|
499
|
+
path por PK) — comportamiento idéntico a antes, 100% retrocompatible.
|
|
500
|
+
Con `filters`, cae a un `SELECT ... WHERE id = :id AND <filtros>` —
|
|
501
|
+
un id que existe pero no matchea el scope devuelve `None` (mismo
|
|
502
|
+
resultado que "no encontrado"), cerrando la clase de IDOR de "conozco
|
|
503
|
+
el id de otro tenant y lo pido directo"."""
|
|
492
504
|
if not self.model:
|
|
493
505
|
raise ValueError("El modelo no está definido en el repositorio")
|
|
494
|
-
|
|
506
|
+
if not filters:
|
|
507
|
+
return await self.session.get(self.model, record_id)
|
|
508
|
+
conditions = [self.model.id == record_id] + self._build_conditions(
|
|
509
|
+
filters=filters
|
|
510
|
+
)
|
|
511
|
+
return await self._get_one(conditions=conditions)
|
|
495
512
|
|
|
496
|
-
async def get_by_id(
|
|
513
|
+
async def get_by_id(
|
|
514
|
+
self,
|
|
515
|
+
record_id: Union[str, UUID],
|
|
516
|
+
filters: Optional[Dict[str, Any]] = None,
|
|
517
|
+
) -> Optional[ModelT]:
|
|
497
518
|
"""Alias de `get(id)` — nombre unificado con el repo Beanie
|
|
498
519
|
(`get_by_id`) para que las lecturas por id sean portables entre ORMs."""
|
|
499
|
-
return await self.get(record_id)
|
|
520
|
+
return await self.get(record_id, filters=filters)
|
|
500
521
|
|
|
501
522
|
async def get_by_field(
|
|
502
523
|
self, field_name: str, value: Any
|
|
@@ -509,11 +530,15 @@ class BaseRepository(Generic[ModelT]):
|
|
|
509
530
|
self,
|
|
510
531
|
record_id: Union[str, UUID],
|
|
511
532
|
joins: Optional[List[str]] = None,
|
|
533
|
+
filters: Optional[Dict[str, Any]] = None,
|
|
512
534
|
) -> Optional[ModelT]:
|
|
513
|
-
"""Obtiene un registro por ID y carga relaciones dinámicamente.
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
535
|
+
"""Obtiene un registro por ID y carga relaciones dinámicamente.
|
|
536
|
+
|
|
537
|
+
`filters` (opcional) — mismo scoping de seguridad que `get()`."""
|
|
538
|
+
conditions = [self.model.id == record_id]
|
|
539
|
+
if filters:
|
|
540
|
+
conditions.extend(self._build_conditions(filters=filters))
|
|
541
|
+
return await self._get_one(conditions=conditions, joins=joins)
|
|
517
542
|
|
|
518
543
|
async def get_by_field_with_joins(
|
|
519
544
|
self,
|
{fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/aio/sqlalchemy/service/base.py
RENAMED
|
@@ -122,9 +122,10 @@ class BaseService(Generic[ModelT]):
|
|
|
122
122
|
if joins is None:
|
|
123
123
|
joins = kwargs.get("joins")
|
|
124
124
|
|
|
125
|
-
|
|
125
|
+
filters = self.get_filters()
|
|
126
|
+
obj = await self.repository.get_with_joins(id, joins=joins, filters=filters)
|
|
126
127
|
if not obj:
|
|
127
|
-
obj = await self.repository.get(id)
|
|
128
|
+
obj = await self.repository.get(id, filters=filters)
|
|
128
129
|
if not obj:
|
|
129
130
|
raise NotFoundException(f"id={id} no encontrado")
|
|
130
131
|
return obj
|
|
@@ -227,6 +228,12 @@ class BaseService(Generic[ModelT]):
|
|
|
227
228
|
return created
|
|
228
229
|
|
|
229
230
|
async def update(self, id: str, data: BaseModel | Dict[str, Any]) -> ModelT:
|
|
231
|
+
# Scoping de seguridad ANTES de tocar nada — `repository.update`
|
|
232
|
+
# fetchea por PK cruda internamente (sin `get_filters()`), así que
|
|
233
|
+
# sin este check un id de otro tenant se podía editar directo.
|
|
234
|
+
scoped = await self.repository.get(id, filters=self.get_filters())
|
|
235
|
+
if not scoped:
|
|
236
|
+
raise NotFoundException(f"id={id} no encontrado")
|
|
230
237
|
update_data = (
|
|
231
238
|
data.model_dump(exclude_unset=True)
|
|
232
239
|
if isinstance(data, BaseModel)
|
|
@@ -277,7 +284,7 @@ class BaseService(Generic[ModelT]):
|
|
|
277
284
|
return True
|
|
278
285
|
|
|
279
286
|
async def delete(self, id: str) -> bool:
|
|
280
|
-
obj = await self.repository.get(id)
|
|
287
|
+
obj = await self.repository.get(id, filters=self.get_filters())
|
|
281
288
|
if not obj:
|
|
282
289
|
raise NotFoundException(message=f"id={id} no encontrado")
|
|
283
290
|
return await self.apply_delete(obj)
|
|
@@ -5,6 +5,34 @@ from fastapi.exceptions import RequestValidationError
|
|
|
5
5
|
from fastapi.responses import JSONResponse
|
|
6
6
|
from pydantic import ValidationError
|
|
7
7
|
|
|
8
|
+
|
|
9
|
+
def _get_cors_headers(request: Request) -> Dict[str, str]:
|
|
10
|
+
"""Extrae headers CORS del ``Origin`` del request para respuestas de error.
|
|
11
|
+
|
|
12
|
+
Cuando una respuesta se genera desde un handler que corre debajo del
|
|
13
|
+
CORSMiddleware los headers CORS se agregan automáticamente. Pero si la
|
|
14
|
+
respuesta se genera desde ServerErrorMiddleware (por encima de CORS) o
|
|
15
|
+
desde cualquier otro camino que no pase por el middleware, los headers
|
|
16
|
+
faltan y el browser reporta un falso error CORS. Esta función permite
|
|
17
|
+
agregarlos defensivamente desde cualquier handler.
|
|
18
|
+
"""
|
|
19
|
+
origin = request.headers.get("origin")
|
|
20
|
+
if not origin:
|
|
21
|
+
return {}
|
|
22
|
+
return {
|
|
23
|
+
"Access-Control-Allow-Origin": origin,
|
|
24
|
+
"Access-Control-Allow-Credentials": "true",
|
|
25
|
+
"Vary": "Origin",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _apply_cors(request: Request, response: JSONResponse) -> JSONResponse:
|
|
30
|
+
"""Agrega headers CORS defensivos a una ``JSONResponse``."""
|
|
31
|
+
headers = _get_cors_headers(request)
|
|
32
|
+
if headers:
|
|
33
|
+
response.headers.update(headers)
|
|
34
|
+
return response
|
|
35
|
+
|
|
8
36
|
try: # pragma: no cover - dependencia opcional
|
|
9
37
|
from pymongo.errors import DuplicateKeyError # type: ignore
|
|
10
38
|
except ImportError: # pragma: no cover
|
|
@@ -35,7 +63,10 @@ async def api_exception_handler(request: Request, exc: APIException):
|
|
|
35
63
|
response = BaseResponse(
|
|
36
64
|
status=exc.status_code, message=exc.message, data=exc.data
|
|
37
65
|
)
|
|
38
|
-
return
|
|
66
|
+
return _apply_cors(
|
|
67
|
+
request,
|
|
68
|
+
JSONResponse(status_code=exc.status, content=response.model_dump()),
|
|
69
|
+
)
|
|
39
70
|
|
|
40
71
|
|
|
41
72
|
async def domain_error_handler(request: Request, exc: DomainError):
|
|
@@ -46,8 +77,11 @@ async def domain_error_handler(request: Request, exc: DomainError):
|
|
|
46
77
|
a `exc.http_status()` con el mismo envelope `BaseResponse` que el resto.
|
|
47
78
|
"""
|
|
48
79
|
response = BaseResponse(status=exc.code, message=exc.message, data=None)
|
|
49
|
-
return
|
|
50
|
-
|
|
80
|
+
return _apply_cors(
|
|
81
|
+
request,
|
|
82
|
+
JSONResponse(
|
|
83
|
+
status_code=exc.http_status(), content=response.model_dump()
|
|
84
|
+
),
|
|
51
85
|
)
|
|
52
86
|
|
|
53
87
|
|
|
@@ -81,9 +115,12 @@ async def document_not_found_handler(request: Request, exc: DocumentNotFound):
|
|
|
81
115
|
message="Documento no encontrado",
|
|
82
116
|
data={"detail": str(exc)},
|
|
83
117
|
)
|
|
84
|
-
return
|
|
85
|
-
|
|
86
|
-
|
|
118
|
+
return _apply_cors(
|
|
119
|
+
request,
|
|
120
|
+
JSONResponse(
|
|
121
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
122
|
+
content=response.model_dump(),
|
|
123
|
+
),
|
|
87
124
|
)
|
|
88
125
|
|
|
89
126
|
|
|
@@ -96,9 +133,12 @@ async def global_exception_handler(request: Request, exc: Exception):
|
|
|
96
133
|
message="Ocurrió un error desconocido",
|
|
97
134
|
data={"detail": str(exc)},
|
|
98
135
|
)
|
|
99
|
-
return
|
|
100
|
-
|
|
101
|
-
|
|
136
|
+
return _apply_cors(
|
|
137
|
+
request,
|
|
138
|
+
JSONResponse(
|
|
139
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
140
|
+
content=response.model_dump(),
|
|
141
|
+
),
|
|
102
142
|
)
|
|
103
143
|
|
|
104
144
|
|
|
@@ -119,9 +159,12 @@ async def value_exception_handler(
|
|
|
119
159
|
data=str(error_details),
|
|
120
160
|
)
|
|
121
161
|
|
|
122
|
-
return
|
|
123
|
-
|
|
124
|
-
|
|
162
|
+
return _apply_cors(
|
|
163
|
+
request,
|
|
164
|
+
JSONResponse(
|
|
165
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
166
|
+
content=response.model_dump(mode="json"),
|
|
167
|
+
),
|
|
125
168
|
)
|
|
126
169
|
|
|
127
170
|
|
|
@@ -139,9 +182,12 @@ async def request_validation_handler(
|
|
|
139
182
|
message="Error de validación",
|
|
140
183
|
data=exc.errors(),
|
|
141
184
|
)
|
|
142
|
-
return
|
|
143
|
-
|
|
144
|
-
|
|
185
|
+
return _apply_cors(
|
|
186
|
+
request,
|
|
187
|
+
JSONResponse(
|
|
188
|
+
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
|
189
|
+
content=response.model_dump(mode="json"),
|
|
190
|
+
),
|
|
145
191
|
)
|
|
146
192
|
|
|
147
193
|
|
|
@@ -153,9 +199,12 @@ async def integrity_error_handler(request: Request, exc: Exception):
|
|
|
153
199
|
message="Registro ya existe o viola una restricción de integridad",
|
|
154
200
|
data={"detail": str(detail) if detail is not None else str(exc)},
|
|
155
201
|
)
|
|
156
|
-
return
|
|
157
|
-
|
|
158
|
-
|
|
202
|
+
return _apply_cors(
|
|
203
|
+
request,
|
|
204
|
+
JSONResponse(
|
|
205
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
206
|
+
content=response.model_dump(mode="json"),
|
|
207
|
+
),
|
|
159
208
|
)
|
|
160
209
|
|
|
161
210
|
|
|
@@ -10,6 +10,7 @@ from fastapi_basekit.exceptions.api_exceptions import (
|
|
|
10
10
|
DatabaseIntegrityException,
|
|
11
11
|
ValidationException,
|
|
12
12
|
)
|
|
13
|
+
from fastapi_basekit.exceptions.handler import _apply_cors # noqa: F401
|
|
13
14
|
from fastapi_basekit.schema.base import BaseResponse
|
|
14
15
|
from pydantic import ValidationError
|
|
15
16
|
{% if cookiecutter.orm == "sqlalchemy" -%}
|
|
@@ -19,7 +20,10 @@ from sqlalchemy.exc import IntegrityError
|
|
|
19
20
|
|
|
20
21
|
async def api_exception_handler(request: Request, exc: APIException):
|
|
21
22
|
response = BaseResponse(status=exc.status_code, message=exc.message, data=exc.data)
|
|
22
|
-
return
|
|
23
|
+
return _apply_cors(
|
|
24
|
+
request,
|
|
25
|
+
JSONResponse(status_code=exc.status, content=response.model_dump()),
|
|
26
|
+
)
|
|
23
27
|
|
|
24
28
|
|
|
25
29
|
async def validation_exception_handler(request: Request, exc: RequestValidationError):
|
|
@@ -28,7 +32,10 @@ async def validation_exception_handler(request: Request, exc: RequestValidationE
|
|
|
28
32
|
|
|
29
33
|
async def database_exception_handler(request: Request, exc: DatabaseIntegrityException):
|
|
30
34
|
response = BaseResponse(status=exc.status_code, message=exc.message, data=exc.data)
|
|
31
|
-
return
|
|
35
|
+
return _apply_cors(
|
|
36
|
+
request,
|
|
37
|
+
JSONResponse(status_code=exc.status, content=response.model_dump()),
|
|
38
|
+
)
|
|
32
39
|
|
|
33
40
|
|
|
34
41
|
async def value_exception_handler(request: Request, exc: Union[ValidationError, ValueError]):
|
|
@@ -42,9 +49,12 @@ async def value_exception_handler(request: Request, exc: Union[ValidationError,
|
|
|
42
49
|
message="Field validation error",
|
|
43
50
|
data=str(error_details),
|
|
44
51
|
)
|
|
45
|
-
return
|
|
46
|
-
|
|
47
|
-
|
|
52
|
+
return _apply_cors(
|
|
53
|
+
request,
|
|
54
|
+
JSONResponse(
|
|
55
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
56
|
+
content=response.model_dump(mode="json"),
|
|
57
|
+
),
|
|
48
58
|
)
|
|
49
59
|
|
|
50
60
|
|
|
@@ -65,9 +75,12 @@ async def integrity_error_handler(request: Request, exc: IntegrityError):
|
|
|
65
75
|
message = "Database integrity error"
|
|
66
76
|
|
|
67
77
|
response = BaseResponse(status="DATABASE_INTEGRITY_ERROR", message=message, data=None)
|
|
68
|
-
return
|
|
69
|
-
|
|
70
|
-
|
|
78
|
+
return _apply_cors(
|
|
79
|
+
request,
|
|
80
|
+
JSONResponse(
|
|
81
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
82
|
+
content=response.model_dump(),
|
|
83
|
+
),
|
|
71
84
|
)
|
|
72
85
|
{% endif %}
|
|
73
86
|
|
|
@@ -78,7 +91,10 @@ async def global_exception_handler(request: Request, exc: Exception):
|
|
|
78
91
|
message="Unknown error",
|
|
79
92
|
data={"detail": str(exc)},
|
|
80
93
|
)
|
|
81
|
-
return
|
|
82
|
-
|
|
83
|
-
|
|
94
|
+
return _apply_cors(
|
|
95
|
+
request,
|
|
96
|
+
JSONResponse(
|
|
97
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
98
|
+
content=response.model_dump(),
|
|
99
|
+
),
|
|
84
100
|
)
|