services/api/src/modules/listings/listing-public-view.js
Overview
Este módulo centraliza la serialización pública de un Listing. Su función principal es construir una forma segura para respuestas HTTP, combinando el enmascarado del anunciante con el saneamiento de contact, location, media, sourceDocuments y requirement.searchZones. El repositorio lo describe como el punto de entrada único usado por GET /api/listings y GET /api/listings/:id, para que el feed y el detalle no diverjan.
El módulo trabaja solo sobre objetos de respuesta. No persiste cambios en Mongo, y sus comentarios lo recalcan varias veces. Para lifecycle.status = anonymized o purged, reduce más campos; para normal y protection, conserva la política pública actual.
[!NOTE]
viewerUserIdsolo sirve para decidir si el visitante es el dueño delListing. El comentario del archivo indica que esa identidad ya debe venir verificada por la ruta, normalmente a partir de un JWT decodificado.
How it works
flowchart TD
A[Listing de entrada] --> B[stripInternalListingFields]
B --> C[maskListingOwner]
C --> D{intent requirement y searchZones}
D -->|si| E[sanitizeSearchZonesForPublicView]
D -->|no| F[conservar requirement]
E --> G[stripMediaFileIds]
F --> G
G --> H[serializeSourceDocuments]
H --> I{lifecycle status anonymized o purged}
I -->|no| J[Devolver listing publico]
I -->|si| K[sanitizeLocation]
I -->|si| L[sanitizeMedia]
I -->|si| M[Vaciar contact y sourceDocuments]
K --> N[Devolver listing enmascarado]
L --> N
M --> N
La función sanitizeListingForPublicView ejecuta el pipeline completo en un orden fijo. Primero quita campos internos, luego enmascara al anunciante, después limita searchZones para visitantes que no son dueños, y por último limpia media y sourceDocuments. Solo al final decide si debe vaciar contact, reducir location y filtrar media tipo map.
Key components
{
"title": "sanitizeListingForPublicView",
"content": "[badge:public] Punto de entrada \u00fanico para serializar un Listing hacia la forma p\u00fablica final. Compone el enmascarado del anunciante con el saneamiento de contacto, ubicaci\u00f3n, media, searchZones y sourceDocuments.",
"type": "function",
"filePath": "services/api/src/modules/listings/listing-public-view.js",
"badges": [
"public"
]
}
{
"title": "stripInternalListingFields",
"content": "[badge:public] Elimina campos internos del Listing y agrega deletionPending para el due\u00f1o cuando la eliminaci\u00f3n est\u00e1 pendiente. No muta el objeto recibido.",
"type": "function",
"filePath": "services/api/src/modules/listings/listing-public-view.js",
"badges": [
"public"
]
}
{
"title": "maskListingOwner",
"content": "[badge:public] Funci\u00f3n importada desde listing-owner-view.js. Sustituye createdBy por una identidad neutral cuando lifecycle.status es anonymized o purged.",
"type": "function",
"filePath": "services/api/src/modules/listings/listing-owner-view.js",
"badges": [
"public"
]
}
{
"title": "serializeSourceDocuments",
"content": "[badge:public] Funci\u00f3n importada desde source-documents.js. Convierte sourceDocuments a la forma p\u00fablica que este m\u00f3dulo vuelve a usar en la respuesta.",
"type": "function",
"filePath": "services/api/src/modules/listings/source-documents.js",
"badges": [
"public"
]
}
Data & fields
[
{
"name": "listing",
"type": "object",
"required": true,
"description": "Listing recibido por el serializador. Puede ser lean o un documento hidratado.",
"children": [
{
"name": "_id",
"type": "string | object",
"description": "Identificador del Listing. El módulo no lo modifica."
},
{
"name": "createdBy",
"type": "object | string | null",
"description": "Autor poblado o referencia al autor. Puede ser reemplazado por una identidad neutral en estados anonymized o purged."
},
{
"name": "intent",
"type": "string",
"description": "Si vale requirement, el módulo también sanea requirement.searchZones."
},
{
"name": "lifecycle",
"type": "object",
"description": "Objeto que controla la política pública del Listing.",
"children": [
{
"name": "status",
"type": "string",
"description": "Si vale anonymized o purged, contact, location, media y sourceDocuments reciben saneamiento adicional."
},
{
"name": "publicOwnerLabel",
"type": "string",
"description": "Etiqueta pública usada por maskListingOwner cuando el anunciante queda anonimizado."
}
]
},
{
"name": "contact",
"type": "object",
"description": "Datos de contacto visibles en la respuesta pública antes del saneamiento de estados protegidos.",
"children": [
{
"name": "name",
"type": "string",
"description": "Nombre público del contacto."
},
{
"name": "phone",
"type": "string",
"description": "Teléfono del contacto."
},
{
"name": "method",
"type": "string",
"description": "Método principal de contacto."
},
{
"name": "link",
"type": "string",
"description": "Enlace externo asociado al contacto."
},
{
"name": "telegramUsername",
"type": "string",
"description": "Usuario de Telegram."
},
{
"name": "message",
"type": "string",
"description": "Mensaje asociado al contacto."
}
]
},
{
"name": "location",
"type": "object",
"description": "Ubicación del Listing.",
"children": [
{
"name": "country",
"type": "string",
"description": "País conservado incluso en la vista reducida."
},
{
"name": "state",
"type": "string",
"description": "Estado conservado incluso en la vista reducida."
},
{
"name": "city",
"type": "string",
"description": "Ciudad conservada incluso en la vista reducida."
},
{
"name": "neighborhood",
"type": "string",
"description": "Colonia o zona. El archivo conserva este campo aunque documenta un riesgo residual de localización."
},
{
"name": "address",
"type": "string",
"description": "Se vacía en estados anonymized o purged."
},
{
"name": "postalCode",
"type": "string",
"description": "Se vacía en estados anonymized o purged."
},
{
"name": "latitude",
"type": "number | null",
"description": "Se nulifica en estados anonymized o purged."
},
{
"name": "longitude",
"type": "number | null",
"description": "Se nulifica en estados anonymized o purged."
},
{
"name": "googleMapsUrl",
"type": "string",
"description": "Se vacía en estados anonymized o purged."
},
{
"name": "mapSnapshotUrl",
"type": "string",
"description": "Se vacía en estados anonymized o purged."
}
]
},
{
"name": "requirement.searchZones",
"type": "array",
"description": "Zonas de búsqueda del requirement. Para visitantes que no son dueños, el centro exacto no se expone.",
"children": [
{
"name": "label",
"type": "string",
"description": "Etiqueta visible de la zona."
},
{
"name": "country",
"type": "string",
"description": "País de la zona."
},
{
"name": "state",
"type": "string",
"description": "Estado de la zona."
},
{
"name": "city",
"type": "string",
"description": "Ciudad de la zona."
},
{
"name": "neighborhood",
"type": "string",
"description": "Colonia de la zona."
},
{
"name": "radiusKm",
"type": "number | null",
"description": "Radio público de búsqueda."
},
{
"name": "center",
"type": "object | null",
"description": "Se conserva completo solo para el dueño; para otros visitantes se reemplaza por null."
}
]
},
{
"name": "media",
"type": "array",
"description": "Lista de medios adjuntos al Listing.",
"children": [
{
"name": "type",
"type": "string",
"description": "Cuando vale map, el elemento se elimina en estados anonymized o purged."
},
{
"name": "fileId",
"type": "string",
"description": "Identificador interno que siempre se retira de la respuesta pública."
}
]
},
{
"name": "sourceDocuments",
"type": "array",
"description": "Documentos fuente del Listing. Se serializan primero y luego se vacían por completo en estados anonymized o purged."
},
{
"name": "deletion",
"type": "object",
"description": "Campo interno que nunca sale en la respuesta pública.",
"children": [
{
"name": "status",
"type": "string",
"description": "Si vale pending y el visitante es el dueño, el serializer expone deletionPending."
}
]
},
{
"name": "conversationReservations",
"type": "array",
"description": "Campo interno eliminado siempre."
},
{
"name": "mediaRevision",
"type": "string | number | object",
"description": "Campo interno eliminado siempre."
},
{
"name": "deletionPending",
"type": "boolean",
"description": "Indicador derivado que solo aparece para el dueño cuando deletion.status es pending."
}
]
},
{
"name": "viewerUserId",
"type": "string | null",
"description": "Identidad verificada del visitante. Solo decide si searchZones.center se conserva completo."
}
]
El archivo define varios campos de salida con política fija. CONTACT_AND_LOCATION_MASKED_STATUSES contiene anonymized y purged. EMPTY_CONTACT es un objeto congelado con campos vacíos. INTERNAL_LISTING_FIELDS elimina deletion, conversationReservations y mediaRevision de cualquier respuesta pública.
Behaviors
sanitizeLocation(location)— devuelve una ubicación reducida concountry,state,cityyneighborhood, y vacíaaddress,postalCode,latitude,longitude,googleMapsUrlymapSnapshotUrl.sanitizeSearchZonesForPublicView(searchZones, { isOwner })— conservasearchZonescompletas para el dueño y, para otros visitantes, mantiene la geografía visible pero reemplazacenterpornull.sanitizeMedia(media)— elimina cualquier item cuyotypeseamap.stripMediaFileIds(media)— recorre la lista de media y eliminafileIdde cada item.stripInternalListingFields(listing, { isOwner = false } = {})— clona el documento, quita campos internos y agregadeletionPending: truesolo si el dueño ve un Listing condeletion.status = pending.sanitizeListingForPublicView(listing, { viewerUserId } = {})— aplica el pipeline completo de serialización pública y nunca muta el Listing original. Paranormalyprotection, devuelve la forma pública sin vaciarcontactni reducirlocation.
Workflows
sequenceDiagram
participant Caller
participant SLPV as sanitizeListingForPublicView
participant Strip as stripInternalListingFields
participant Mask as maskListingOwner
participant Zones as sanitizeSearchZonesForPublicView
participant Media as stripMediaFileIds
participant Docs as serializeSourceDocuments
participant Loc as sanitizeLocation
participant Filter as sanitizeMedia
Caller->>SLPV: listing and viewerUserId
SLPV->>Strip: remove internal fields
Strip-->>SLPV: listing without internals
SLPV->>Mask: mask owner by lifecycle status
Mask-->>SLPV: owner masked or unchanged
SLPV->>Zones: sanitize requirement search zones when present
Zones-->>SLPV: zones for owner or public
SLPV->>Media: strip media fileId
Media-->>SLPV: media without fileId
SLPV->>Docs: serialize source documents
Docs-->>SLPV: public source documents
alt lifecycle status is anonymized or purged
SLPV->>Loc: reduce location
Loc-->>SLPV: reduced location
SLPV->>Filter: remove map media
Filter-->>SLPV: media without map items
SLPV-->>Caller: masked public listing
else lifecycle status is normal or protection
SLPV-->>Caller: public listing
end
El flujo es deliberadamente determinista. El módulo primero prepara una copia pública estable y solo después aplica el bloqueo extra por estado de ciclo de vida. Eso permite que la misma función sirva para feed, detalle y otros consumidores públicos.
Gotchas & edge cases
[!WARNING]
viewerUserIdno debe tomarse de un valor sin verificar. El archivo aclara que la ruta debe decodificar y validar la identidad antes de pasársela a este serializer.
[!CAUTION]
location.neighborhoodpermanece visible en la versión reducida. El propio código documenta un riesgo residual de reidentificación en barrios pequeños o poco densos.
[!IMPORTANT]
mediase limpia en dos niveles distintos. Primero se eliminafileIdsiempre, y luego se expulsan los elementostype = mapcuando el Listing estáanonymizedopurged.
[!NOTE]
stripInternalListingFieldsusatoJSON()cuando existe. El comentario explica que eso replica exactamente la forma queres.jsonproduciría para un documento hidratado.
[!WARNING]
sourceDocumentsse serializa antes del bloqueo por estado, pero enanonymizedypurgedel resultado final se reemplaza por un arreglo vacío.
Related
services/api/src/modules/listings/listing-owner-view.jsaportamaskListingOwner, que sustituyecreatedBypor una identidad neutral enanonymizedypurged.services/api/src/modules/listings/source-documents.jsaportaserializeSourceDocuments, que este módulo usa antes del saneamiento final desourceDocuments.services/api/src/modules/listings/listing-public-assembly.jsreutilizasanitizeListingForPublicViewpara compartir la misma forma pública entreGET /api/listings,GET /api/listings/:idy el consumidor de Match.services/api/test/listing-public-view.test.jsyservices/api/test/listing-public-assembly.test.jsvalidan el comportamiento de esta serialización compartida.
[!NOTE] Este archivo no define endpoints HTTP. Su responsabilidad es exclusivamente transformar objetos
Listingantes de que la capa de rutas los entregue al cliente.