services/api/src/scripts/seed.js

Overview

Este script prepara datos locales para MongoDB. Crea o actualiza usuarios de prueba, genera anuncios, y rellena media faltante para que el backoffice tenga contenido visible. Está pensado para ejecutarse varias veces sin duplicar usuarios ni los anuncios del administrador .

[!IMPORTANT] Este seed solo escribe datos locales en MongoDB. No crea Customers ni Subscriptions de Stripe, y no requiere claves de Stripe. Las cuentas con plan pagado pueden existir sin datos de Stripe detrás. Eso es intencional para pruebas .

El archivo también carga services/api/.env desde su propia ubicación y luego hace una segunda carga de dotenv. Así mantiene la misma configuración aunque cambie el directorio de ejecución fileciteturn0file3turn0file7.

[!NOTE] [email protected] es la cuenta seed principal. [email protected] y [email protected] son cuentas de prueba para anuncios. [email protected] no se crea aquí. Esa cuenta queda fuera del seed a propósito para probar Stripe e ImageKit reales .

How it works

flowchart TD
    Start[Inicio]
    Env[dotenv config]
    Guard[evaluateSeedTarget]
    Reject[Rechazar destino]
    Connect[mongoose connect]
    AdminPass[resolveSeedPassword]
    AdminHash[bcrypt hash admin]
    AdminUpsert[upsertSeedUser admin]
    AdminListings[Insertar anuncios admin]
    SofiaUpsert[upsertTestUser Sofia]
    SofiaListing[ensureListing Sofia]
    CarlosUpsert[upsertTestUser Carlos]
    CarlosListing[ensureListing Carlos]
    FixMedia[Actualizar media vacia]
    Disconnect[mongoose disconnect]
    End[Fin]

    Start --> Env
    Env --> Guard
    Guard -->|no| Reject
    Guard -->|si| Connect
    Reject --> End
    Connect --> AdminPass
    AdminPass --> AdminHash
    AdminHash --> AdminUpsert
    AdminUpsert --> AdminListings
    AdminListings --> SofiaUpsert
    SofiaUpsert --> SofiaListing
    SofiaListing --> CarlosUpsert
    CarlosUpsert --> CarlosListing
    CarlosListing --> FixMedia
    FixMedia --> Disconnect
    Disconnect --> End

El flujo empieza con la validación del destino. evaluateSeedTarget se ejecuta antes de conectar y bloquea destinos no locales. Si el destino no es aceptado, el script escribe los motivos, fija process.exitCode = 1 y termina sin conectar fileciteturn0file14turn0file3.

Si el destino es válido, el script conecta con mongoose. Después resuelve la contraseña del admin, genera su hash con bcrypt, y crea o actualiza [email protected] con findOneAndUpdate y upsert: true fileciteturn0file3turn0file7.

Luego completa los anuncios del administrador hasta llegar a 15, salvo que el admin legado ya conserve anuncios. Después asegura un anuncio para Sofia y otro para Carlos. Al final repara listings que no tienen media y desconecta Mongoose siempre, incluso tras errores fileciteturn0file3turn0file13.

Key components

Cada componente resuelve una parte concreta del seed. Juntos forman un flujo idempotente para desarrollo local fileciteturn0file3turn0file7.

{
    "title": "resolveSeedPassword",
    "content": "Resuelve la contrase\u00f1a del seed. Usa el valor de entorno si existe; si no, genera una temporal para esa corrida y no la imprime.",
    "type": "function",
    "filePath": "services/api/src/scripts/seed.js",
    "badges": [
        "public"
    ]
}
{
    "title": "slugify",
    "content": "Normaliza un texto para usarlo como semilla estable. Pasa a min\u00fasculas, elimina diacr\u00edticos, reemplaza caracteres no alfanum\u00e9ricos y recorta guiones extremos.",
    "type": "function",
    "filePath": "services/api/src/scripts/seed.js",
    "badges": [
        "public"
    ]
}
{
    "title": "buildPlaceholderMedia",
    "content": "Genera dos im\u00e1genes placeholder estables usando picsum.photos. La semilla sale del t\u00edtulo normalizado del anuncio.",
    "type": "function",
    "filePath": "services/api/src/scripts/seed.js",
    "badges": [
        "public"
    ]
}
{
    "title": "buildListing",
    "content": "Construye el documento de anuncio. Define precio, ubicaci\u00f3n, descripci\u00f3n, media, meta y estado publicado.",
    "type": "function",
    "filePath": "services/api/src/scripts/seed.js",
    "badges": [
        "public"
    ]
}
{
    "title": "buildUser",
    "content": "Construye la base de un documento de usuario. Une nombre y apellido, y aplica valores por defecto para systemRole y accountRole.",
    "type": "function",
    "filePath": "services/api/src/scripts/seed.js",
    "badges": [
        "public"
    ]
}
{
    "title": "upsertSeedUser",
    "content": "Busca un usuario por email y lo crea o actualiza. Inserta la identidad solo al crear, y corrige plan, roles y estado cuando ya existe.",
    "type": "function",
    "filePath": "services/api/src/scripts/seed.js",
    "badges": [
        "public"
    ]
}
{
    "title": "upsertTestUser",
    "content": "Crea o actualiza Sofia y Carlos. Si existe una contrase\u00f1a de entorno, la rehace; si no, conserva el hash existente cuando el usuario ya existe.",
    "type": "function",
    "filePath": "services/api/src/scripts/seed.js",
    "badges": [
        "public"
    ]
}
{
    "title": "ensureListing",
    "content": "Garantiza un anuncio m\u00ednimo para un usuario. Solo crea uno si el usuario no tiene anuncios previos.",
    "type": "function",
    "filePath": "services/api/src/scripts/seed.js",
    "badges": [
        "public"
    ]
}
{
    "title": "run",
    "content": "Orquesta todo el seed. Valida el destino, conecta MongoDB, prepara usuarios, crea anuncios, repara media y cierra la conexi\u00f3n.",
    "type": "function",
    "filePath": "services/api/src/scripts/seed.js",
    "badges": [
        "public"
    ]
}
{
    "title": "evaluateSeedTarget",
    "content": "Valida que el seed apunte a MongoDB local de desarrollo. El script no contin\u00faa si el destino es remoto, ambiguo o no interpretable.",
    "type": "function",
    "filePath": "services/api/src/scripts/lib/seedTargetGuard.js",
    "badges": [
        "public"
    ]
}

Data & fields

El script usa variables de entorno, plantillas de anuncios y objetos base de usuario. Estas estructuras determinan qué se crea y cómo queda guardado en MongoDB fileciteturn0file7turn0file15.

Variables de entorno

[
    {
        "name": "MONGODB_URI",
        "type": "string",
        "description": "URI de MongoDB. Si no existe, el script usa mongodb://127.0.0.1:27017/plano_backoffice."
    },
    {
        "name": "NODE_ENV",
        "type": "string",
        "description": "Entrada adicional para la guardia de destino. Valores de producción o staging se rechazan."
    },
    {
        "name": "APP_ENV",
        "type": "string",
        "description": "Entrada adicional para la guardia de destino. Valores de producción o staging se rechazan."
    },
    {
        "name": "SEED_PASSWORD",
        "type": "string",
        "description": "Fuente principal de contraseña para [email protected]."
    },
    {
        "name": "ADMIN_PASSWORD",
        "type": "string",
        "description": "Fallback de contraseña para [email protected] si SEED_PASSWORD no existe."
    },
    {
        "name": "TEST_USER_SOFIA_PASSWORD",
        "type": "string",
        "description": "Contraseña opcional para [email protected]."
    },
    {
        "name": "TEST_USER_CARLOS_PASSWORD",
        "type": "string",
        "description": "Contraseña opcional para [email protected]."
    }
]

Identidades fijas del seed

[
    {
        "name": "ADMIN_EMAIL",
        "type": "string",
        "required": true,
        "description": "Email fijo del admin seed. El archivo lo define como [email protected]."
    },
    {
        "name": "LEGACY_ADMIN_EMAIL",
        "type": "string",
        "required": true,
        "description": "Email legado usado solo para verificar si ya existen anuncios transferidos. El archivo lo exporta como [email protected]."
    }
]

Plantillas de anuncios

ADMIN_LISTINGS, SOFIA_LISTING y CARLOS_LISTING comparten la misma forma. Cambian solo los valores concretos de cada inmueble fileciteturn0file3turn0file15.

[
    {
        "name": "title",
        "type": "string",
        "required": true,
        "description": "Título visible del anuncio."
    },
    {
        "name": "city",
        "type": "string",
        "required": true,
        "description": "Ciudad del inmueble."
    },
    {
        "name": "state",
        "type": "string",
        "required": true,
        "description": "Estado o entidad del inmueble."
    },
    {
        "name": "amount",
        "type": "number",
        "required": true,
        "description": "Monto del anuncio en MXN."
    },
    {
        "name": "op",
        "type": "string",
        "required": true,
        "description": "Tipo de operación. En el archivo aparecen sale, presale, rent e investment_offer."
    },
    {
        "name": "prop",
        "type": "string",
        "required": true,
        "description": "Tipo de propiedad. El archivo usa house, apartment, land, commercial, office, warehouse y other."
    },
    {
        "name": "bedrooms",
        "type": "number",
        "description": "Número de recámaras. Solo existe en algunas plantillas."
    },
    {
        "name": "bathrooms",
        "type": "number",
        "description": "Número de baños. Solo existe en algunas plantillas."
    }
]

buildUser input

[
    {
        "name": "firstName",
        "type": "string",
        "required": true,
        "description": "Nombre del usuario."
    },
    {
        "name": "lastName",
        "type": "string",
        "required": true,
        "description": "Apellido del usuario."
    },
    {
        "name": "email",
        "type": "string",
        "required": true,
        "description": "Email único del usuario."
    },
    {
        "name": "phone",
        "type": "string",
        "required": true,
        "description": "Teléfono del usuario."
    },
    {
        "name": "username",
        "type": "string",
        "required": true,
        "description": "Username del usuario."
    },
    {
        "name": "passwordHash",
        "type": "string",
        "required": true,
        "description": "Hash de contraseña ya calculado."
    },
    {
        "name": "systemRole",
        "type": "string",
        "description": "Rol de sistema. Por defecto es user."
    },
    {
        "name": "accountRole",
        "type": "string",
        "description": "Rol de cuenta. Por defecto es creator."
    }
]

buildListing output

[
    {
        "name": "title",
        "type": "string",
        "required": true,
        "description": "Título del anuncio."
    },
    {
        "name": "createdBy",
        "type": "ObjectId",
        "required": true,
        "description": "Referencia al usuario dueño del anuncio."
    },
    {
        "name": "price",
        "type": "object",
        "required": true,
        "description": "Precio del anuncio.",
        "children": [
            {
                "name": "amount",
                "type": "number",
                "required": true,
                "description": "Cantidad en MXN."
            },
            {
                "name": "currency",
                "type": "string",
                "required": true,
                "description": "Moneda fija MXN."
            }
        ]
    },
    {
        "name": "location",
        "type": "object",
        "required": true,
        "description": "Ubicación del inmueble.",
        "children": [
            {
                "name": "city",
                "type": "string",
                "required": true,
                "description": "Ciudad."
            },
            {
                "name": "state",
                "type": "string",
                "required": true,
                "description": "Estado."
            },
            {
                "name": "country",
                "type": "string",
                "required": true,
                "description": "País fijo Mexico."
            }
        ]
    },
    {
        "name": "description",
        "type": "object",
        "required": true,
        "description": "Texto corto del anuncio.",
        "children": [
            {
                "name": "short",
                "type": "string",
                "required": true,
                "description": "Resumen corto."
            },
            {
                "name": "caption",
                "type": "string",
                "required": true,
                "description": "Leyenda visible."
            }
        ]
    },
    {
        "name": "media",
        "type": "array",
        "required": true,
        "description": "Dos imágenes placeholder estables.",
        "children": [
            {
                "name": "type",
                "type": "string",
                "required": true,
                "description": "Siempre image."
            },
            {
                "name": "src",
                "type": "string",
                "required": true,
                "description": "URL de picsum.photos con semilla derivada del título."
            },
            {
                "name": "alt",
                "type": "string",
                "required": true,
                "description": "Texto alternativo igual al título."
            }
        ]
    },
    {
        "name": "meta",
        "type": "object",
        "required": true,
        "description": "Metadatos del inmueble.",
        "children": [
            {
                "name": "entityType",
                "type": "string",
                "required": true,
                "description": "Siempre property."
            },
            {
                "name": "operationType",
                "type": "string",
                "required": true,
                "description": "Tipo de operación original."
            },
            {
                "name": "propertyType",
                "type": "string",
                "required": true,
                "description": "Tipo de propiedad original."
            },
            {
                "name": "bedrooms",
                "type": "number",
                "required": true,
                "description": "Número de recámaras o 0."
            },
            {
                "name": "bathrooms",
                "type": "number",
                "required": true,
                "description": "Número de baños o 0."
            }
        ]
    },
    {
        "name": "publication",
        "type": "object",
        "required": true,
        "description": "Estado de publicación.",
        "children": [
            {
                "name": "status",
                "type": "string",
                "required": true,
                "description": "Siempre published."
            }
        ]
    }
]

Behaviors

Cada función tiene una responsabilidad concreta. El script compone esas piezas para mantener el seed repetible y predecible fileciteturn0file3turn0file9turn0file12.

  • resolveSeedPassword — devuelve una contraseña explícita o genera una temporal para la corrida actual.
  • slugify — convierte un título en una semilla estable para URLs de imágenes.
  • buildPlaceholderMedia — devuelve dos objetos de media con URLs deterministas de picsum.photos.
  • buildListing — devuelve un documento plano listo para Listing.create o insertMany.
  • buildUser — devuelve un documento base de usuario con name, systemRole y accountRole.
  • upsertSeedUser — crea o actualiza [email protected] sin sobrescribir datos personales existentes.
  • upsertTestUser — crea o actualiza Sofia y Carlos, y puede rehacer el hash si recibe contraseña de entorno.
  • ensureListing — garantiza al menos un listing por usuario, sin duplicar si ya existe alguno.
  • run — ejecuta todo el flujo y fija process.exitCode cuando la guardia falla.

[!NOTE] upsertSeedUser usa $setOnInsert para la identidad. En ejecuciones posteriores, solo corrige plan, systemRole, accountRole y accountStatus. No pisa nombre, email, username, teléfono ni password de usuarios ya existentes fileciteturn0file7turn0file9.

[!WARNING] upsertTestUser trata distinto a los usuarios existentes. Si envPassword no está definida, conserva el hash existente. También evita imprimir contraseñas temporales. Esa contraseña temporal solo se usa cuando el usuario aún no existe .

[!CAUTION] ensureListing no intenta completar múltiples anuncios. Solo crea uno si el usuario no tiene ninguno. Si ya existe al menos un anuncio, no duplica registros .

Workflows

1. Cargar entorno | Lee services/api/.env y luego hace una segunda carga de dotenv.
2. Validar destino | Ejecuta evaluateSeedTarget antes de conectar.
3. Rechazar destino inseguro | Si no es local de desarrollo, escribe razones y termina sin conectar.
4. Conectar MongoDB | Abre mongoose con MONGO_URI.
5. Preparar admin | Hashea ADMIN_PASSWORD y crea o actualiza [email protected].
6. Completar anuncios | Inserta anuncios del administrador hasta 15, sin duplicar los existentes.
7. Asegurar cuentas demo | Crea o actualiza Sofia y Carlos y garantiza un anuncio para cada uno.
8. Reparar media | Busca listings sin media y les agrega imágenes placeholder.
9. Cerrar proceso | Desconecta mongoose al final, incluso si ocurrió un error.

El flujo es idempotente en varias partes. Los usuarios se reescriben por upsert, el admin se completa solo hasta 15 anuncios, y los listings vacíos reciben media de forma posterior. El cierre final siempre desconecta Mongoose en finally fileciteturn0file13turn0file4.

Gotchas & edge cases

[!WARNING] La guardia de destino bloquea cualquier MongoDB que no sea local de desarrollo. También rechaza destinos ambiguos o ilegibles. El mensaje de rechazo solo muestra hosts y base. Nunca imprime la URI completa ni credenciales fileciteturn0file14turn0file3.

[!IMPORTANT] El script no crea documentos en admin_users. Tampoco recrea la cuenta legado de Aaron. LEGACY_ADMIN_EMAIL solo sirve para detectar si ya existen anuncios asociados a esa identidad .

[!NOTE] [email protected] se crea con systemRole=admin. Sofia y Carlos se guardan con systemRole=user. buildUser deja accountRole por defecto en creator fileciteturn0file3turn0file7.

[!CAUTION] El script rellena listings sin media usando imágenes placeholder. Esa reparación solo actúa sobre anuncios existentes con media ausente o vacía .

Estos archivos participan directamente en este seed o reciben sus datos.

  • services/api/src/utils/passwords.js — aporta hashPassword para Sofia y Carlos.
  • services/api/src/models/User.js — recibe los upserts de usuarios.
  • services/api/src/models/Listing.js — recibe los anuncios creados por el seed.
  • services/api/src/scripts/lib/seedTargetGuard.js — valida que el destino sea local.
  • services/api/.env — primera fuente de configuración para el script.
  • services/api/package.json — expone npm run seed como comando de ejecución

[!TIP] El comando del proyecto apunta directamente a node src/scripts/seed.js. Eso convierte a este archivo en la entrada operativa del seed local fileciteturn0file16turn0file17