Modelo Conversation

Resumen

services/api/src/models/Conversation.js define el modelo Mongoose Conversation y sus esquemas embebidos. Representa el contexto e historial privado de un usuario dentro de la cuenta. Guarda mensajes, el activeRequirementId, estados operativos y referencias mínimas a requisitos asociados.

[!IMPORTANT] Conversation no actúa como motor de negocio. Solo conserva historial privado y referencias mínimas. No persiste candidatos de Match, ni su orden, ni explicaciones, ni snapshots de Listings.

Cómo funciona

El archivo construye primero restricciones y esquemas internos. Después define el documento principal conversationSchema con referencias a User y Listing. Mongoose aplica validaciones, límites de longitud, valores permitidos e índices antes de guardar.

flowchart TB
  User[User]
  Conversation[Conversation]
  Messages[conversationMessageSchema]
  Listing[Listing]
  Pending[pendingAttachSchema]
  Deleted[deletedRequirementSchema]

  User -->|owns| Conversation
  Conversation -->|activeRequirementId| Listing
  Conversation -->|messages| Messages
  Messages -->|requirementId| Listing
  Conversation -->|pendingAttach| Pending
  Pending -->|requirementId| Listing
  Conversation -->|deletedRequirement| Deleted
  Deleted -->|requirementId| Listing

La relación principal es simple. ownerId identifica al usuario dueño. messages guarda el historial embebido. pendingAttach conserva una reserva interna y oculta. deletedRequirement mantiene una referencia histórica mínima cuando un requisito activo desaparece.

[!NOTE] Los eventos requirement_changed, requirement_refined y requirement_deleted son emitidos por el servidor. El cliente no elige role ni kind.

Componentes clave

{
    "title": "Conversation",
    "content": "Modelo Mongoose principal del archivo. Persist\u0435 el historial privado de un usuario, el requisito activo y metadatos de conversaci\u00f3n en la colecci\u00f3n `conversations`.",
    "type": "class",
    "filePath": "services/api/src/models/Conversation.js",
    "badges": [
        "public"
    ]
}
{
    "title": "conversationMessageSchema",
    "content": "Esquema embebido para cada mensaje de `messages`. Valida `role`, `kind`, `text`, `requirementId`, `changedFields` y `createdAt`.",
    "type": "class",
    "filePath": "services/api/src/models/Conversation.js",
    "badges": []
}
{
    "title": "pendingAttachSchema",
    "content": "Esquema interno para una reserva temporal de asociaci\u00f3n con un `Requirement`. Incluye `requirementId`, `token`, `at` y `expiresAt`.",
    "type": "class",
    "filePath": "services/api/src/models/Conversation.js",
    "badges": []
}
{
    "title": "deletedRequirementSchema",
    "content": "Esquema m\u00ednimo para registrar un requisito activo eliminado. Conserva `requirementId`, `title` y `deletedAt`.",
    "type": "class",
    "filePath": "services/api/src/models/Conversation.js",
    "badges": []
}

Datos y campos

[
    {
        "name": "ownerId",
        "type": "mongoose.Schema.Types.ObjectId ref User",
        "required": true,
        "description": "Usuario propietario de la Conversation."
    },
    {
        "name": "title",
        "type": "string",
        "description": "Título visible de la conversación. Se recorta y admite hasta 120 caracteres."
    },
    {
        "name": "activeRequirementId",
        "type": "mongoose.Schema.Types.ObjectId ref Listing",
        "description": "Referencia al Listing activo con intent requirement. No copia datos del Requirement."
    },
    {
        "name": "status",
        "type": "string",
        "description": "Estado de la conversación. Solo admite active o archived."
    },
    {
        "name": "messages",
        "type": "array",
        "description": "Historial embebido de mensajes y eventos de sistema. Cada elemento tiene su propio `_id`.",
        "children": [
            {
                "name": "role",
                "type": "string",
                "required": true,
                "description": "Rol del mensaje. Solo admite user o system."
            },
            {
                "name": "kind",
                "type": "string",
                "description": "Tipo del mensaje. Por defecto es text."
            },
            {
                "name": "text",
                "type": "string",
                "description": "Contenido del mensaje. Tiene máximo de 2000 caracteres y es obligatorio cuando kind es text."
            },
            {
                "name": "requirementId",
                "type": "mongoose.Schema.Types.ObjectId ref Listing",
                "description": "Referencia al Requirement afectado por el evento. Se usa en requirement_changed, requirement_refined y requirement_deleted."
            },
            {
                "name": "changedFields",
                "type": "string array",
                "description": "Nombres de los campos requirement.* que cambiaron. Solo aplica a requirement_refined."
            },
            {
                "name": "createdAt",
                "type": "Date",
                "description": "Marca temporal del mensaje. Se asigna con Date.now."
            }
        ]
    },
    {
        "name": "pendingAttach",
        "type": "object",
        "description": "Reserva interna de una asociación en curso con un Requirement. No tiene valor por defecto y usa select false.",
        "children": [
            {
                "name": "requirementId",
                "type": "mongoose.Schema.Types.ObjectId ref Listing",
                "required": true,
                "description": "Requirement reservado."
            },
            {
                "name": "token",
                "type": "mongoose.Schema.Types.ObjectId",
                "required": true,
                "description": "Token interno para confirmar o invalidar la reserva."
            },
            {
                "name": "at",
                "type": "Date",
                "required": true,
                "description": "Fecha en que se creó la reserva."
            },
            {
                "name": "expiresAt",
                "type": "Date",
                "required": true,
                "description": "Fecha de expiración de la reserva."
            }
        ]
    },
    {
        "name": "deletedRequirement",
        "type": "object",
        "description": "Referencia histórica mínima al Requirement activo eliminado. No es un snapshot.",
        "children": [
            {
                "name": "requirementId",
                "type": "mongoose.Schema.Types.ObjectId ref Listing",
                "required": true,
                "description": "Identificador del Requirement eliminado."
            },
            {
                "name": "title",
                "type": "string",
                "description": "Título histórico del Requirement. Por defecto es null."
            },
            {
                "name": "deletedAt",
                "type": "Date",
                "required": true,
                "description": "Momento en que el Requirement se eliminó."
            }
        ]
    }
]

Los campos embebidos siguen dos reglas importantes. messages conserva el historial completo de la conversación. pendingAttach y deletedRequirement no tienen _id propio, porque son metadatos internos y compactos.

Constantes exportadas

Estas constantes fijan límites y vocabularios válidos para el modelo. El archivo las exporta junto con Conversation, para que otros módulos reutilicen las mismas reglas.

  • CONVERSATION_TITLE_MAX_LENGTH — 120. Límite de longitud para title.
  • CONVERSATION_MESSAGE_MAX_LENGTH — 2000. Límite de longitud para messages.text.
  • CONVERSATION_MAX_MESSAGES — 200. Tope total de mensajes embebidos por Conversation.
  • CONVERSATION_STATUSES — ['active', 'archived']. Estados permitidos para status.
  • CONVERSATION_MESSAGE_ROLES — ['user', 'system']. Roles válidos para messages.role.
  • CONVERSATION_MESSAGE_KINDS — ['text', 'requirement_changed', 'requirement_refined', 'requirement_deleted']. Tipos válidos para messages.kind.

[!TIP] CONVERSATION_MAX_MESSAGES se aplica de forma atómica en el filtro del $push, según el comentario del archivo. Eso mantiene el límite en escrituras concurrentes.

Comportamientos

Este archivo no define métodos propios de instancia ni métodos estáticos. Su comportamiento se concentra en validación, referencia y persistencia del documento.

  • mongoose.model('Conversation', conversationSchema) — registra y exporta el modelo Conversation.
  • conversationSchema.index({ ownerId: 1, updatedAt: -1 }) — crea un índice para listar conversaciones por propietario y recencia.
  • title.trim y title.maxlength — normalizan y limitan el título.
  • status.enum — restringe el estado a active o archived.
  • messages.role.enum — restringe el rol a user o system.
  • messages.kind.enum — restringe el tipo de mensaje a los valores declarados.
  • messages.text.validate — exige texto no vacío cuando kind es text.
  • pendingAttach.select: false — oculta la reserva interna en consultas normales.
  • timestamps: true — agrega createdAt y updatedAt al documento principal.
  • collection: 'conversations' — fija la colección de MongoDB usada por el modelo.

Flujo de trabajo

Este archivo describe un flujo de persistencia, no una secuencia de llamadas HTTP. El flujo combina validación de schema, referencias y almacenamiento embebido.

1. Crear Conversation | Se asignan `ownerId`, `title`, `status` y referencias iniciales.
2. Agregar mensaje | Mongoose valida `role`, `kind` y `text` dentro de `messages`.
3. Registrar evento interno | Los tipos `requirement_changed`, `requirement_refined` y `requirement_deleted` usan `requirementId`.
4. Guardar metadatos privados | `pendingAttach` y `deletedRequirement` conservan información interna mínima.
5. Persistir marcas temporales | `timestamps` agrega `createdAt` y `updatedAt` al documento principal.

Riesgos y casos límite

La estructura tiene varias restricciones sensibles. La más importante es que algunos campos solo existen para uso interno del servidor.

[!WARNING] pendingAttach tiene select: false. No aparece en lecturas normales, salvo que la consulta lo solicite explícitamente.

[!WARNING] messages.text es obligatorio solo cuando kind es text. Los eventos de sistema pueden no usar el mismo contenido que un mensaje de usuario.

[!NOTE] requirementId en messages, pendingAttach y deletedRequirement es una referencia. No guarda una copia completa del requisito.

[!CAUTION] changedFields solo guarda nombres de campos. No guarda valores antes o después. Tampoco guarda snapshots del Requirement.

[!NOTE] El comentario del archivo indica que la Conversation es dato privado de la cuenta. También indica su purga definitiva en el día 60.

Relacionado

Este modelo se conecta con otros componentes nombrados en el propio archivo. Los comentarios citan conversations.routes.js, requirement-refinement.js y purgeExpiredAccounts.js como puntos de uso o integración. También referencia los modelos User y Listing mediante ref.

  • User — origen de ownerId.
  • Listing — origen de activeRequirementId, requirementId, pendingAttach.requirementId y deletedRequirement.requirementId.
  • conversations.routes.js — aplica el límite atómico de mensajes, según el comentario del archivo.
  • requirement-refinement.js — emite requirement_refined, según el comentario del archivo.
  • purgeExpiredAccounts.js — elimina el dato en la purga definitiva, según el comentario del archivo.