Skip to content

[IMP] pms_fastapi: guard free-text searches with min length (RFC 9457)#102

Draft
jesusVMayor wants to merge 1 commit into
16.0from
feat/fastapi-search-text-guard
Draft

[IMP] pms_fastapi: guard free-text searches with min length (RFC 9457)#102
jesusVMayor wants to merge 1 commit into
16.0from
feat/fastapi-search-text-guard

Conversation

@jesusVMayor

Copy link
Copy Markdown
Member

Qué

Guarda escalable de longitud mínima para las búsquedas por texto de la API
(pms_fastapi), con error en formato RFC 9457 problem+json para que el front pueda
distinguir el caso "texto demasiado corto".

Antes solo phone tenía Query(min_length=3) y globalSearch un if len >= 3 suelto:
no escalaba a todos los campos y devolvía un 422 genérico de Pydantic.

Cómo

  • Nuevo tipo marcador SearchText (schemas/base.py): se anota el parámetro de texto
    libre con SearchText en lugar de str | None.
  • PmsApiRouter (models/fastapi_endpoint.py) envuelve automáticamente cualquier
    endpoint con un parámetro BaseSearch: si un campo marcado trae un valor no vacío de
    longitud < MIN_SEARCH_TEXT_LENGTH (3), devuelve un problem+json antes de
    ejecutar el endpoint. Sin if por campo, sin código por endpoint, y los endpoints de
    búsqueda futuros quedan cubiertos solos.
  • Campos marcados en: contact, agency, customer, guest, supplier, invoice y folio
    (params de texto migrados al estilo Annotated[...]). Vacío/whitespace = sin filtro
    (no error).

Respuesta de error (ejemplo exacto)

GET /contacts?name=ab

HTTP/1.1 400 Bad Request
Content-Type: application/problem+json
{
  "type": "/errors/search-text-too-short",
  "title": "Search text too short",
  "status": 400,
  "detail": "Search text must be at least 3 characters.",
  "field": "name",
  "minLength": 3
}

field es el nombre del query param ofensor (p. ej. name, globalSearch, room).
Para el front, la señal a detectar es type == "/errors/search-text-too-short".

Tests

  • tests/test_search_text_guard.py (end-to-end vía FastAPITransactionCase): casos 400
    (ambos estilos de anotación), 200, vacío, y param no marcado.
  • Suite completa de pms_fastapi en verde: 0 failed, 0 error(s) of 56 tests.

Convención documentada

Se actualiza la skill roomdoo-fastapi-conventions con la sección de errores RFC 9457 y
la guarda de búsqueda.


⚠️ Antes de mergear: cohesionar con otras PR de la API

Esta PR toca los schemas de búsqueda (*Search) de pms_fastapi y migra sus
parámetros de texto al estilo Annotated[...]. Hay otras PR abiertas que también
modifican endpoints/schemas de la API y que podrían:

  • añadir nuevos campos de búsqueda de texto (habrá que marcarlos con SearchText),
  • modificar las firmas de los mismos *Search (posibles conflictos de merge en
    contact/agency/customer/guest/supplier/invoice/folio),
  • introducir errores que deberían adoptar el mismo formato RFC 9457.

Acción requerida antes del merge: revisar las PR de la API que estén en vuelo,
resolver conflictos y asegurar que los campos de texto nuevos quedan marcados con
SearchText y que los errores siguen la convención problem+json. Se mantiene en
DRAFT hasta esa reconciliación.

🤖 Generated with Claude Code

Free-text search params shorter than MIN_SEARCH_TEXT_LENGTH (3) were only
guarded ad-hoc (Query(min_length) on phone + a stray globalSearch check),
which did not scale and surfaced a generic 422.

Mark free-text query params with the new SearchText type; PmsApiRouter wraps
any endpoint with a BaseSearch param and rejects too-short values with an
RFC 9457 problem+json (/errors/search-text-too-short) before the endpoint
runs. No per-field/per-endpoint code; future search endpoints are covered.

Applied to contact, agency, customer, guest, supplier, invoice and folio
searches (moved to Annotated[...] param style). Adds an end-to-end test and
documents the convention in the roomdoo-fastapi-conventions skill.
@github-actions

Copy link
Copy Markdown

Diff Coverage

Diff: origin/16.0...HEAD, staged and unstaged changes

  • pms_fastapi/models/fastapi_endpoint.py (100%)
  • pms_fastapi/schemas/agency.py (33.3%): Missing lines 99-100
  • pms_fastapi/schemas/base.py (100%)
  • pms_fastapi/schemas/contact.py (33.3%): Missing lines 366-367
  • pms_fastapi/schemas/customer.py (33.3%): Missing lines 118-119
  • pms_fastapi/schemas/guest.py (33.3%): Missing lines 187-188
  • pms_fastapi/schemas/invoice.py (100%)
  • pms_fastapi/schemas/pms_folio.py (100%)
  • pms_fastapi/schemas/supplier.py (33.3%): Missing lines 120-121

Summary

  • Total: 73 lines
  • Missing: 10 lines
  • Coverage: 86%

pms_fastapi/schemas/agency.py

Lines 95-104

   95                 ("display_name", "ilike", self.globalSearch),
   96                 ("email", "ilike", self.globalSearch),
   97                 ("vat", "ilike", self.globalSearch),
   98             ]
!  99             phone_domain = [("phone_mobile_search", "ilike", self.globalSearch)]
! 100             domain = expression.OR([domain, phone_domain])
  101         if self.name:
  102             domain.append(("display_name", "ilike", self.name))
  103         if self.phone:
  104             domain.append(("phone_mobile_search", "ilike", self.phone))

pms_fastapi/schemas/contact.py

Lines 362-371

  362                 ("email", "ilike", self.globalSearch),
  363                 ("vat", "ilike", self.globalSearch),
  364                 ("identification_number", "ilike", self.globalSearch),
  365             ]
! 366             phone_domain = [("phone_mobile_search", "ilike", self.globalSearch)]
! 367             domain = expression.OR([domain, phone_domain])
  368         if self.name:
  369             domain.append(("display_name", "ilike", self.name))
  370         if self.phone:
  371             domain.append(("phone_mobile_search", "ilike", self.phone))

pms_fastapi/schemas/customer.py

Lines 114-123

  114                 ("display_name", "ilike", self.globalSearch),
  115                 ("email", "ilike", self.globalSearch),
  116                 ("vat", "ilike", self.globalSearch),
  117             ]
! 118             phone_domain = [("phone_mobile_search", "ilike", self.globalSearch)]
! 119             domain = expression.OR([domain, phone_domain])
  120         if self.vat:
  121             domain.append(("vat", "ilike", self.vat))
  122         if self.name:
  123             domain.append(("display_name", "ilike", self.name))

pms_fastapi/schemas/guest.py

Lines 183-192

  183                 ("email", "ilike", self.globalSearch),
  184                 ("vat", "ilike", self.globalSearch),
  185                 ("identification_number", "ilike", self.globalSearch),
  186             ]
! 187             phone_domain = [("phone_mobile_search", "ilike", self.globalSearch)]
! 188             domain = expression.OR([domain, phone_domain])
  189         if self.vat:
  190             id_numbers = (
  191                 env["res.partner.id_number"]
  192                 .sudo()

pms_fastapi/schemas/supplier.py

Lines 116-125

  116                 ("display_name", "ilike", self.globalSearch),
  117                 ("email", "ilike", self.globalSearch),
  118                 ("vat", "ilike", self.globalSearch),
  119             ]
! 120             phone_domain = [("phone_mobile_search", "ilike", self.globalSearch)]
! 121             domain = expression.OR([domain, phone_domain])
  122         if self.vat:
  123             domain.append(("vat", "ilike", self.vat))
  124         if self.name:
  125             domain.append(("display_name", "ilike", self.name))

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant