services/api/src/scripts/migrateLegacyEmailVerification.js
Overview
Este archivo implementa una migración explícita para cuentas legacy de la Fase 7B. Su objetivo es retro-poblar emailVerifiedAt con createdAt solo para usuarios antiguos que nunca pasaron por el flujo real de verificación. El script se ejecuta desde consola y está expuesto como npm run migrate:legacy-email-verification.
[!IMPORTANT] La migración no usa solo
emailVerifiedAt: null. También exigecreatedAt < cutoffDateyaccountStatusválido. Esa combinación evita tocar cuentas nuevas o cuentas purgadas.
How it works
flowchart TB
Start([Inicio])
LoadEnv[Carga dotenv]
ParseArgs[Lee flags de consola]
Connect[Conecta a MongoDB]
RunFunc[runMigrateLegacyEmailVerification]
BuildQuery[Construye filtro de usuarios]
DryRun[Modo dry run]
Apply[Modo apply]
Batch[Procesa lotes]
Update[User updateOne]
ErrorLog[Log seguro de error]
Summary[Imprime summary JSON]
Disconnect[Desconecta MongoDB]
Start --> LoadEnv --> ParseArgs --> Connect --> RunFunc --> BuildQuery
BuildQuery --> DryRun --> Summary
BuildQuery --> Apply --> Batch --> Update --> Batch
Update --> ErrorLog --> Batch
DryRun --> Summary
Apply --> Summary
Summary --> Disconnect
El flujo comienza solo cuando el módulo se ejecuta directamente. Primero carga variables de entorno, luego resuelve MONGODB_URI, --apply, --dry-run, --batch-size y --cutoff. Después abre la conexión con mongoose, ejecuta la función de migración y finalmente imprime un resumen JSON y cierra la conexión.
La función principal usa un filtro determinista sobre User. En dry-run, solo pagina resultados y cuenta coincidencias. En apply, actualiza cada usuario válido con emailVerifiedAt = createdAt. Si una escritura falla, el script guarda el _id afectado y registra solo un código seguro y el userId.
Key components
{
"title": "runMigrateLegacyEmailVerification",
"content": "Funci\u00f3n p\u00fablica que localiza usuarios legacy elegibles y, en modo apply, fija emailVerifiedAt con createdAt. Devuelve un resumen con contadores de coincidencias, migraciones, errores e IDs fallidos.",
"type": "function",
"filePath": "services/api/src/scripts/migrateLegacyEmailVerification.js",
"badges": [
"public"
]
}
LEGACY_CUTOFF_ISO— constante con el cutoff por defecto:2026-08-12T00:00:00.000Z. Sirve como fecha base cuando no se pasa--cutoff.ELIGIBLE_ACCOUNT_STATUSES— lista interna con los estados permitidos:activeypending_deletion. El script excluye cualquier otro estado.isMainModule— guard que evita ejecutar la migración al importar el archivo desde tests u otros módulos.
Data & fields
[
{
"name": "options",
"type": "object",
"description": "Opciones de ejecución para la migración.",
"children": [
{
"name": "dryRun",
"type": "boolean",
"description": "Si es true, el script solo cuenta coincidencias y no modifica documentos. Valor por defecto: true."
},
{
"name": "batchSize",
"type": "number",
"description": "Tamaño del lote para paginar usuarios. Valor por defecto: 50."
},
{
"name": "cutoffDate",
"type": "Date",
"description": "Fecha límite usada para excluir cuentas nuevas. Por defecto usa LEGACY_CUTOFF_ISO."
}
]
}
]
[
{
"name": "summary",
"type": "object",
"required": true,
"description": "Resultado final que imprime el script en JSON.",
"children": [
{
"name": "mode",
"type": "string",
"required": true,
"description": "Modo de ejecución: dry-run o apply."
},
{
"name": "batchSize",
"type": "number",
"required": true,
"description": "Tamaño de lote usado durante la ejecución."
},
{
"name": "cutoffDate",
"type": "string",
"required": true,
"description": "Cutoff final serializado en ISO-8601."
},
{
"name": "matched",
"type": "number",
"required": true,
"description": "Cantidad de documentos que cumplieron el filtro."
},
{
"name": "migrated",
"type": "number",
"required": true,
"description": "Cantidad de documentos actualizados con éxito."
},
{
"name": "errors",
"type": "number",
"required": true,
"description": "Cantidad de fallos de actualización."
},
{
"name": "errorUserIds",
"type": "string[]",
"required": true,
"description": "IDs de usuarios que fallaron durante apply."
}
]
}
]
[
{
"name": "emailVerifiedAt",
"type": "null",
"required": true,
"description": "El campo debe estar en null. En MongoDB, esto también puede coincidir con campo ausente."
},
{
"name": "createdAt",
"type": "object",
"required": true,
"description": "Debe ser anterior al cutoff.",
"children": [
{
"name": "$lt",
"type": "Date",
"required": true,
"description": "Limita la búsqueda a usuarios creados antes de cutoffDate."
}
]
},
{
"name": "accountStatus",
"type": "object",
"required": true,
"description": "Restringe la migración a estados elegibles.",
"children": [
{
"name": "$in",
"type": "string[]",
"required": true,
"description": "Valores permitidos: active y pending_deletion."
}
]
}
]
Behaviors
runMigrateLegacyEmailVerification— consulta usuarios candidatos, pagina resultados por lotes y retorna un resumen con contadores y errores. Enapply, escribeemailVerifiedAt = createdAtpor documento.dotenv.config— carga primero../../.envrelativo al script y luego variables del entorno actual.mongoose.connect— abre la conexión antes de ejecutar la migración.mongoose.disconnect— cierra la conexión en unfinally, incluso si la migración falla.console.error— registra fallos de escritura con un código seguro y eluserId, sin exponer email, nombre ni teléfono.
Workflows
1. Cargar entorno | El script carga dotenv desde `../../.env` y desde el entorno actual.
2. Resolver opciones | Lee `--apply`, `--dry-run`, `--batch-size` y `--cutoff`.
3. Validar cutoff | Rechaza el valor si no es una fecha ISO-8601 válida.
4. Conectar MongoDB | Usa `MONGODB_URI` o el URI local por defecto.
5. Ejecutar migración | Llama a `runMigrateLegacyEmailVerification` con las opciones calculadas.
6. Mostrar resultado | Imprime el resumen final como JSON con sangría.
7. Cerrar conexión | Ejecuta `mongoose.disconnect` en un bloque `finally`.
En modo dry-run, el código avanza con skip, porque no cambia el filtro entre iteraciones. En modo apply, cada usuario exitoso deja de cumplir el filtro después de actualizarse, y los fallidos se excluyen con _id: { $nin: failedIds }. Eso evita bucles infinitos.
Gotchas & edge cases
[!WARNING] En MongoDB,
{ emailVerifiedAt: null }también puede coincidir con documentos donde el campo no existe. Por eso el script añadeaccountStatusycreatedAtal filtro. Sin esa protección, cuentas purgadas o anonimizadas podrían quedar marcadas como verificadas por error.
dryRunes el valor por defecto si no se pasa--apply. Eso significa que el script no escribe nada salvo que la ejecución sea explícitamente aplicada.--cutoffdebe ser una fecha ISO-8601 válida. Si no lo es, el proceso lanza un error antes de conectar.- El script solo incluye cuentas con
accountStatusactiveopending_deletion. Otros estados quedan excluidos por diseño. - La migración escribe
emailVerifiedAtcon el propiocreatedAtdel usuario. No usa una fecha derivada ni un valor fijo.
Related
Este script se expone en services/api/package.json como migrate:legacy-email-verification, junto a otros scripts de mantenimiento del backend. Su comportamiento está cubierto por services/api/test/migrate-legacy-email-verification.test.js, que valida cuentas active, pending_deletion, exclusiones por estado, cutoff, campo ya verificado, modo dry-run e idempotencia.
El comentario del propio script lo conecta con purgeExpiredAccounts.js, porque ese proceso puede hacer $unset de emailVerifiedAt. Esa relación explica por qué el filtro exige estado de cuenta explícito.