listing-write-sanitizer.js

Overview

Este archivo define la barrera de escritura para listings en services/api/src/modules/listings/listing-write-sanitizer.js. Su función es limpiar el cuerpo enviado por el cliente antes de que llegue a POST y PATCH /api/listings.

El sanitizador aplica una allowlist explícita. Solo conserva campos permitidos y descarta el resto. Así evita que el cliente reasigne campos que pertenecen al servidor o que no forman parte del contrato de escritura.

[!IMPORTANT] El comentario del archivo marca varios campos como propiedad del servidor. Esos campos no deben venir desde el cliente, aunque estén presentes en el body.

  • _id
  • createdBy
  • lifecycle
  • metrics
  • submissionId
  • sourceDocuments
  • createdAt
  • updatedAt
  • publication.verified
  • pdf.url

[!NOTE] El archivo no depende del modo strict de Mongoose. La restricción se aplica antes de Listing.create y findOneAndUpdate.

How it works

flowchart TB
    A[Incoming body] --> B[isPlainObject check]
    B -->|No| C[Return empty object]
    B -->|Yes| D[pick allowed root fields]
    D --> E[Sanitize publication]
    D --> F[Sanitize pdf]
    E --> G[Return payload]
    F --> G[Return payload]

El flujo empieza con una validación básica del valor recibido. Si el body no es un objeto plano, el archivo devuelve un objeto vacío. Si sí lo es, copia solo los campos de primer nivel permitidos.

Después, el sanitizador trata dos objetos anidados de forma separada: publication y pdf. En ambos casos, solo conserva los subcampos autorizados. Todo valor extra se elimina en el resultado final.

Key components

{
    "title": "sanitizeClientListingPayload",
    "content": "Funci\u00f3n exportada que recibe el body del cliente, conserva solo los campos permitidos y devuelve un payload limpio para escritura.",
    "type": "function",
    "filePath": "services/api/src/modules/listings/listing-write-sanitizer.js",
    "badges": [
        "public"
    ]
}
{
    "title": "isPlainObject",
    "content": "Funci\u00f3n interna que comprueba si un valor es un objeto plano y no un array.",
    "type": "function",
    "filePath": "services/api/src/modules/listings/listing-write-sanitizer.js",
    "badges": []
}
{
    "title": "pick",
    "content": "Funci\u00f3n interna que copia solo las claves permitidas y solo si existen con valor distinto de undefined.",
    "type": "function",
    "filePath": "services/api/src/modules/listings/listing-write-sanitizer.js",
    "badges": []
}

Data & fields

El archivo define tres allowlists. Dos controlan objetos anidados, y una controla los campos de primer nivel. El sanitizador no valida tipos de negocio. Solo filtra claves y conserva valores tal como llegan.

  • CLIENT_WRITABLE_FIELDS [badge:read-only] string[] — campos de primer nivel que el cliente puede enviar.
  • CLIENT_WRITABLE_PUBLICATION_FIELDS [badge:read-only] string[] — subcampos permitidos dentro de publication.
  • CLIENT_WRITABLE_PDF_FIELDS [badge:read-only] string[] — subcampos permitidos dentro de pdf.
[
    {
        "name": "title",
        "type": "any",
        "description": "Campo de primer nivel conservado desde el body del cliente."
    },
    {
        "name": "intent",
        "type": "any",
        "description": "Campo de primer nivel conservado desde el body del cliente."
    },
    {
        "name": "price",
        "type": "any",
        "description": "Campo de primer nivel conservado desde el body del cliente."
    },
    {
        "name": "location",
        "type": "any",
        "description": "Campo de primer nivel conservado desde el body del cliente."
    },
    {
        "name": "description",
        "type": "any",
        "description": "Campo de primer nivel conservado desde el body del cliente."
    },
    {
        "name": "contact",
        "type": "any",
        "description": "Campo de primer nivel conservado desde el body del cliente."
    },
    {
        "name": "media",
        "type": "any",
        "description": "Campo de primer nivel conservado desde el body del cliente."
    },
    {
        "name": "pdf",
        "type": "object",
        "description": "Objeto opcional. Si no es un objeto plano, se reemplaza por un objeto vacío.",
        "children": [
            {
                "name": "fileName",
                "type": "any",
                "description": "Nombre del archivo PDF generado."
            },
            {
                "name": "generatedAt",
                "type": "any",
                "description": "Marca temporal de generación del PDF."
            }
        ]
    },
    {
        "name": "meta",
        "type": "any",
        "description": "Campo de primer nivel conservado desde el body del cliente."
    },
    {
        "name": "requirement",
        "type": "any",
        "description": "Campo de primer nivel conservado desde el body del cliente."
    },
    {
        "name": "publication",
        "type": "object",
        "description": "Objeto opcional. Si no es un objeto plano, se reemplaza por un objeto vacío.",
        "children": [
            {
                "name": "status",
                "type": "any",
                "description": "Estado de publicación permitido por el cliente."
            },
            {
                "name": "role",
                "type": "any",
                "description": "Rol de publicación permitido por el cliente."
            }
        ]
    }
]
  • title
  • intent
  • price
  • location
  • description
  • contact
  • media
  • pdf
  • meta
  • requirement
  • publication
  • status
  • role
  • fileName
  • generatedAt

Behaviors

  • isPlainObject(value) — devuelve true solo para objetos no nulos y no array.
  • pick(source, keys) — crea un objeto nuevo con las claves permitidas cuyo valor no es undefined.
  • sanitizeClientListingPayload(body) [badge:public] — devuelve {} si el body no es un objeto plano; si lo es, filtra campos raíz y sanitiza publication y pdf.

Gotchas & edge cases

[!WARNING] Si body no es un objeto plano, el resultado es {}. Esto incluye arrays, null, undefined y valores primitivos.

[!WARNING] publication y pdf solo conservan sus claves permitidas. Si llegan como valores no objeto, se convierten en {}.

[!CAUTION] El sanitizador no convierte tipos ni valida contenido semántico. Solo elimina claves no permitidas.

[!IMPORTANT] sourceDocuments no se escribe desde este sanitizador. El comentario del archivo indica que solo la subruta /source-documents trata ese dato.

Este archivo actúa como control previo a la persistencia de listings. El comentario lo vincula con POST/PATCH /api/listings, Listing.create y findOneAndUpdate. También separa claramente el manejo de sourceDocuments en la subruta /source-documents.

La relación funcional principal es esta:

  • Entrada del cliente en POST o PATCH /api/listings
  • Limpieza con sanitizeClientListingPayload
  • Persistencia con un payload ya filtrado
  • Exclusión de campos del servidor y de subcampos no autorizados