ListingEvent

Overview

services/api/src/models/ListingEvent.js define un modelo Mongoose para registrar eventos de actividad sobre una Listing. El esquema persiste quién recibió el evento, qué acción ocurrió, desde qué origen llegó y si ya fue leído. También fija la colección listing_events y activa timestamps automáticos.

Este modelo sirve como historial conductual de una publicación. Sus valores de type y source muestran que cubre interacciones como like, comment, qr_scan, pdf_download, contact_click, detail_open y profile_open.

[!NOTE] El archivo no contiene lógica de negocio adicional. Solo define el esquema, sus índices y la exportación del modelo Mongoose.

How it works

El archivo construye un Schema de Mongoose, define los campos del evento y después registra el modelo con mongoose.model("ListingEvent", ListingEventSchema). Mongoose guarda los documentos en la colección listing_events y añade automáticamente createdAt y updatedAt por timestamps: true.

El esquema también declara dos índices. Uno organiza la consulta por ownerId y createdAt descendente. El otro combina listingId, type y createdAt descendente para soportar la agregación del digest descrita en el comentario del código.

Key components

{
    "title": "ListingEvent",
    "content": "Modelo Mongoose que persiste eventos de actividad en la colecci\u00f3n listing_events y expone el historial conductual de una publicaci\u00f3n.",
    "type": "class",
    "filePath": "services/api/src/models/ListingEvent.js",
    "badges": [
        "public"
    ]
}

Data & fields

[
    {
        "name": "listingId",
        "type": "Schema.Types.ObjectId",
        "required": true,
        "description": "Referencia obligatoria a \"Listing\". Identifica la publicación relacionada con el evento."
    },
    {
        "name": "ownerId",
        "type": "Schema.Types.ObjectId",
        "required": true,
        "description": "Referencia obligatoria a \"User\". Representa al propietario del evento."
    },
    {
        "name": "actorId",
        "type": "Schema.Types.ObjectId",
        "description": "Referencia opcional a \"User\" que ejecutó la acción. Por defecto es null."
    },
    {
        "name": "actorName",
        "type": "String",
        "description": "Nombre visible del actor. Usa trim y por defecto es una cadena vacía."
    },
    {
        "name": "type",
        "type": "String",
        "required": true,
        "description": "Tipo de evento. Valores permitidos: like, unlike, comment, qr_scan, pdf_download, contact_click, detail_open y profile_open."
    },
    {
        "name": "source",
        "type": "String",
        "description": "Origen del evento. Valores permitidos: feed, drawer, profile, pdf y qr. Por defecto es feed."
    },
    {
        "name": "metadata",
        "type": "Schema.Types.Mixed",
        "description": "Carga útil libre para información adicional. Por defecto es un objeto vacío."
    },
    {
        "name": "read",
        "type": "Boolean",
        "description": "Marca de lectura del evento. Por defecto es false."
    }
]

Campos obligatorios

  • listingId [badge:required] Schema.Types.ObjectId — referencia a la Listing asociada al evento.
  • ownerId [badge:required] Schema.Types.ObjectId — referencia al User propietario del registro.
  • type [badge:required] String — clasifica el evento dentro del conjunto permitido por el esquema.

Campos opcionales

  • actorId [badge:optional] Schema.Types.ObjectId — identifica al User que realizó la acción, si existe.
  • actorName [badge:optional] String — guarda el nombre visible del actor, con trim: true.
  • source [badge:optional] String — registra el contexto de origen del evento.
  • metadata [badge:optional] Schema.Types.Mixed — almacena datos auxiliares sin estructura fija.
  • read [badge:optional] Boolean — indica si el evento ya fue marcado como leído.

Valores permitidos

type

  • like
  • unlike
  • comment
  • qr_scan
  • pdf_download
  • contact_click
  • detail_open
  • profile_open

source

  • feed
  • drawer
  • profile
  • pdf
  • qr

Behaviors

  • mongoose.Schema(...) — define la forma del documento y sus validaciones de enum, requerido y valores por defecto.
  • ListingEventSchema.index({ ownerId: 1, createdAt: -1 }) — acelera lecturas recientes por propietario.
  • ListingEventSchema.index({ listingId: 1, type: 1, createdAt: -1 }) — soporta la agregación del digest por publicación, tipo y ventana temporal.
  • mongoose.model("ListingEvent", ListingEventSchema) — registra y exporta el modelo ListingEvent.

Persistence details

El esquema persiste en la colección explícita listing_events. La opción timestamps: true añade createdAt y updatedAt automáticamente a cada documento.

[!IMPORTANT] El índice compuesto { listingId: 1, type: 1, createdAt: -1 } no es decorativo. El comentario del archivo explica que evita escanear todos los eventos de una publicación cuando el digest agrupa por tipo dentro de una ventana temporal.

Relationships

ListingEvent se relaciona directamente con Listing mediante listingId. También se relaciona con User por ownerId y actorId, lo que permite distinguir al propietario del evento y al actor que produjo la interacción.

El diseño del esquema muestra que el modelo está orientado a registrar actividad contextual sobre una publicación. La presencia de source, type y read confirma que no solo almacena el evento, sino también su procedencia y su estado de lectura.

Gotchas & edge cases

[!WARNING] listingId, ownerId y type son obligatorios. Un documento incompleto no cumple el esquema.

[!WARNING] metadata usa Schema.Types.Mixed. El esquema no restringe su forma interna.

[!CAUTION] actorId puede ser null. El campo existe aunque no haya un actor asociado.

[!NOTE] read nace en false. Los eventos nuevos se crean como no leídos.

  • services/api/src/models/Listing.js aparece como referencia en listingId.
  • services/api/src/models/User.js aparece como referencia en ownerId y actorId.
  • services/api/src/models/ListingEvent.js es el único archivo documentado aquí y define toda la persistencia de este modelo.

[!NOTE] En el código revisado no aparecen endpoints HTTP dentro de este archivo. Por eso no se incluye una sección de API.