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.
Files changed (148) hide show
  1. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/PKG-INFO +1 -1
  2. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/aio/beanie/repository/base.py +113 -71
  3. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/aio/beanie/service/base.py +39 -3
  4. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/aio/controller/base.py +27 -7
  5. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/aio/sqlalchemy/repository/base.py +34 -9
  6. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/aio/sqlalchemy/service/base.py +10 -3
  7. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/exceptions/handler.py +67 -18
  8. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/utils/exception_handlers.py +27 -11
  9. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit.egg-info/PKG-INFO +1 -1
  10. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit.egg-info/SOURCES.txt +2 -0
  11. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/pyproject.toml +1 -1
  12. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/tests/test_beanie_service_regression.py +73 -0
  13. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/tests/test_exception_handlers.py +17 -0
  14. fastapi_basekit-0.5.4/tests/test_format_response_schema_override.py +79 -0
  15. fastapi_basekit-0.5.4/tests/test_owner_filter_helper.py +65 -0
  16. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/tests/test_sqlalchemy_service_regression.py +73 -0
  17. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/LICENSE +0 -0
  18. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/README.md +0 -0
  19. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/__init__.py +0 -0
  20. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/aio/__init__.py +0 -0
  21. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/aio/beanie/__init__.py +0 -0
  22. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/aio/beanie/controller/__init__.py +0 -0
  23. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/aio/beanie/controller/base.py +0 -0
  24. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/aio/beanie/repository/__init__.py +0 -0
  25. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/aio/beanie/service/__init__.py +0 -0
  26. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/aio/controller/__init__.py +0 -0
  27. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/aio/permissions/__init__.py +0 -0
  28. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/aio/permissions/base.py +0 -0
  29. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/aio/sqlalchemy/__init__.py +0 -0
  30. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/aio/sqlalchemy/controller/__init__.py +0 -0
  31. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/aio/sqlalchemy/controller/base.py +0 -0
  32. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/aio/sqlalchemy/repository/__init__.py +0 -0
  33. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/aio/sqlalchemy/service/__init__.py +0 -0
  34. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/aio/sqlalchemy/session.py +0 -0
  35. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/aio/sqlmodel/__init__.py +0 -0
  36. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/aio/sqlmodel/controller/__init__.py +0 -0
  37. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/aio/sqlmodel/controller/base.py +0 -0
  38. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/aio/sqlmodel/repository/__init__.py +0 -0
  39. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/aio/sqlmodel/repository/base.py +0 -0
  40. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/aio/sqlmodel/service/__init__.py +0 -0
  41. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/aio/sqlmodel/service/base.py +0 -0
  42. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/cli/__init__.py +0 -0
  43. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/cli/main.py +0 -0
  44. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/exceptions/__init__.py +0 -0
  45. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/exceptions/api_exceptions.py +0 -0
  46. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/exceptions/domain.py +0 -0
  47. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/openapi.py +0 -0
  48. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/py.typed +0 -0
  49. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/schema/__init__.py +0 -0
  50. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/schema/base.py +0 -0
  51. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/schema/jwt.py +0 -0
  52. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/schema/schema.py +0 -0
  53. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/servicios/__init__.py +0 -0
  54. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/servicios/thrid/__init__.py +0 -0
  55. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/servicios/thrid/jwt.py +0 -0
  56. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/cookiecutter.json +0 -0
  57. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/hooks/post_gen_project.py +0 -0
  58. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/hooks/pre_gen_project.py +0 -0
  59. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/.env.example +0 -0
  60. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/.gitignore +0 -0
  61. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/Dockerfile +0 -0
  62. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/LICENSE +0 -0
  63. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/Makefile +0 -0
  64. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/README.md +0 -0
  65. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/alembic/env.py +0 -0
  66. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/alembic/script.py.mako +0 -0
  67. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/alembic.ini +0 -0
  68. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/__init__.py +0 -0
  69. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/__init__.py +0 -0
  70. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/v1/__init__.py +0 -0
  71. {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
  72. {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
  73. {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
  74. {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
  75. {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
  76. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/api/v1/routers.py +0 -0
  77. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/config/__init__.py +0 -0
  78. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/config/database.py +0 -0
  79. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/config/settings.py +0 -0
  80. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/main.py +0 -0
  81. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/middleware/__init__.py +0 -0
  82. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/middleware/auth.py +0 -0
  83. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/middleware/permissions.py +0 -0
  84. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/models/__init__.py +0 -0
  85. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/models/auth.py +0 -0
  86. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/models/base.py +0 -0
  87. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/models/enums.py +0 -0
  88. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/models/types.py +0 -0
  89. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/permissions/__init__.py +0 -0
  90. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/permissions/user.py +0 -0
  91. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/repositories/__init__.py +0 -0
  92. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/repositories/user/__init__.py +0 -0
  93. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/repositories/user/repository.py +0 -0
  94. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/schemas/__init__.py +0 -0
  95. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/schemas/auth.py +0 -0
  96. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/schemas/base.py +0 -0
  97. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/schemas/user.py +0 -0
  98. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/scripts/__init__.py +0 -0
  99. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/scripts/init.py +0 -0
  100. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/scripts/init_admin.py +0 -0
  101. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/services/__init__.py +0 -0
  102. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/services/auth_service.py +0 -0
  103. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/services/dependency.py +0 -0
  104. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/services/user_service.py +0 -0
  105. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/utils/__init__.py +0 -0
  106. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/app/utils/security.py +0 -0
  107. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/docker-compose.yml +0 -0
  108. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/pytest.ini +0 -0
  109. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit/templates/project/{{cookiecutter.project_slug}}/requirements.txt +0 -0
  110. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit.egg-info/dependency_links.txt +0 -0
  111. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit.egg-info/entry_points.txt +0 -0
  112. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit.egg-info/requires.txt +0 -0
  113. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/fastapi_basekit.egg-info/top_level.txt +0 -0
  114. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/setup.cfg +0 -0
  115. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/tests/test_action_param_leak.py +0 -0
  116. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/tests/test_api_exceptions.py +0 -0
  117. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/tests/test_base_response.py +0 -0
  118. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/tests/test_base_service.py +0 -0
  119. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/tests/test_beanie_aggregation_hooks.py +0 -0
  120. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/tests/test_beanie_aggregation_integration.py +0 -0
  121. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/tests/test_beanie_keyset_pagination.py +0 -0
  122. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/tests/test_beanie_repository_regression.py +0 -0
  123. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/tests/test_beanie_service_mutable_defaults.py +0 -0
  124. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/tests/test_controller_auto_permissions.py +0 -0
  125. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/tests/test_crud_beanie_controller.py +0 -0
  126. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/tests/test_crud_controller.py +0 -0
  127. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/tests/test_crud_sqlmodel_repository_service.py +0 -0
  128. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/tests/test_filter_operators.py +0 -0
  129. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/tests/test_generics_optional_and_cache.py +0 -0
  130. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/tests/test_generics_wiring.py +0 -0
  131. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/tests/test_jwt_config_robustness.py +0 -0
  132. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/tests/test_jwt_service.py +0 -0
  133. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/tests/test_openapi_simplify.py +0 -0
  134. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/tests/test_params_classification.py +0 -0
  135. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/tests/test_params_coercion_matrix.py +0 -0
  136. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/tests/test_params_coercion_unit.py +0 -0
  137. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/tests/test_params_http_matrix_beanie.py +0 -0
  138. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/tests/test_params_http_matrix_sqlalchemy.py +0 -0
  139. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/tests/test_params_rich_type_coercion.py +0 -0
  140. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/tests/test_path_filter_mixin_compat.py +0 -0
  141. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/tests/test_post_process_list_hook.py +0 -0
  142. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/tests/test_pulbot_cbv_pattern_beanie.py +0 -0
  143. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/tests/test_repo_signature_aliases.py +0 -0
  144. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/tests/test_sql_queryset_override.py +0 -0
  145. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/tests/test_sqlalchemy_base_service_order.py +0 -0
  146. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/tests/test_sqlalchemy_repository_regression.py +0 -0
  147. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/tests/test_sqlmodel_filter_operators.py +0 -0
  148. {fastapi_basekit-0.5.2 → fastapi_basekit-0.5.4}/tests/test_typed_filter_coercion.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fastapi-basekit
3
- Version: 0.5.2
3
+ Version: 0.5.4
4
4
  Summary: Utilities and base classes for FastAPI async projects (Beanie, SQLAlchemy or SQLModel)
5
5
  Author-email: Jerson Moreno <jerson.ml820@hotmail.com>
6
6
  License: MIT
@@ -119,75 +119,60 @@ class BaseRepository(Generic[ModelT]):
119
119
  kwargs["projection"] = projection
120
120
  return kwargs
121
121
 
122
- def build_filter_query(
123
- self,
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
- def _is_link_field(field_name: str) -> bool:
155
- """Verifica si un campo es de tipo Link."""
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
- return False
136
+ # Caso directo: Link[Model]
137
+ if origin is Link:
138
+ return True
178
139
 
179
- raw_filters: Dict[str, Any] = {}
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
- def _coerce_objectid(value: Any) -> Any:
182
- """str/UUID → ObjectId para queries Mongo sobre Link.id."""
183
- if isinstance(value, ObjectId):
184
- return value
185
- if isinstance(value, str):
186
- try:
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 sin filtrar → fuga cross-tenant (IDOR). El warning hace
222
- # visible la pérdida en logs/Sentry en vez de fallar en silencio.
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
- "build_filter_query: filtro '%s' descartado (no resuelve a "
225
- "ningún campo de %s). Si era un filtro de scoping, el listado "
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
- query_args: list = ([raw_filters] if raw_filters else []) + exprs
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(id, **kwargs)
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(id, **kwargs)
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
- if isinstance(data, list):
268
- data_dicts = [self.to_dict(item) for item in data]
269
- try:
270
- adapter = TypeAdapter(List[schema])
271
- data_parsed = adapter.validate_python(data_dicts)
272
- except Exception:
273
- data_parsed = data_dicts
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:
@@ -487,16 +487,37 @@ class BaseRepository(Generic[ModelT]):
487
487
 
488
488
  return record
489
489
 
490
- async def get(self, record_id: Union[str, UUID]) -> Optional[ModelT]:
491
- """Obtiene un registro por su ID."""
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
- return await self.session.get(self.model, record_id)
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(self, record_id: Union[str, UUID]) -> Optional[ModelT]:
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
- return await self._get_one(
515
- conditions=[self.model.id == record_id], joins=joins
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,
@@ -122,9 +122,10 @@ class BaseService(Generic[ModelT]):
122
122
  if joins is None:
123
123
  joins = kwargs.get("joins")
124
124
 
125
- obj = await self.repository.get_with_joins(id, joins=joins)
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 JSONResponse(status_code=exc.status, content=response.model_dump())
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 JSONResponse(
50
- status_code=exc.http_status(), content=response.model_dump()
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 JSONResponse(
85
- status_code=status.HTTP_404_NOT_FOUND,
86
- content=response.model_dump(),
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 JSONResponse(
100
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
101
- content=response.model_dump(),
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 JSONResponse(
123
- status_code=status.HTTP_400_BAD_REQUEST,
124
- content=response.model_dump(mode="json"),
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 JSONResponse(
143
- status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
144
- content=response.model_dump(mode="json"),
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 JSONResponse(
157
- status_code=status.HTTP_400_BAD_REQUEST,
158
- content=response.model_dump(mode="json"),
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 JSONResponse(status_code=exc.status, content=response.model_dump())
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 JSONResponse(status_code=exc.status, content=response.model_dump())
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 JSONResponse(
46
- status_code=status.HTTP_400_BAD_REQUEST,
47
- content=response.model_dump(mode="json"),
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 JSONResponse(
69
- status_code=status.HTTP_400_BAD_REQUEST,
70
- content=response.model_dump(),
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 JSONResponse(
82
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
83
- content=response.model_dump(),
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
  )