services/api/src/scripts/purgeExpiredAccounts.js
Overview
Este script ejecuta la purga final de cuentas que ya pasaron por la fase de anonimización y quedaron listas para eliminación definitiva. Busca usuarios con accountStatus: 'anonymized_pending_purge', purgeScheduledAt <= now y deletionCompletedAt: null, y procesa cada cuenta por lotes. El archivo exporta runPurgeExpiredAccounts para pruebas y uso programático, y solo abre la conexión a MongoDB cuando se ejecuta directamente como CLI.
[!IMPORTANT] El modo por defecto es
dry-run. La mutación real solo ocurre con--apply. El flujo mantiene la idempotencia porque el filtro exigedeletionCompletedAt: null, y las cuentas ya purgadas dejan de coincidir en corridas posteriores.
El código también aplica una política estricta de saneamiento. Reemplaza email y phone por placeholders deterministas derivados de derivePurgedIdentifierDigest, para no romper índices únicos. Además, limpia campos sensibles del usuario, elimina conversaciones, purga leads y vacía datos públicos de listings asociados.
How it works
sequenceDiagram
participant CLI as CLI entrypoint
participant Env as dotenv
participant DB as mongoose
participant Job as runPurgeExpiredAccounts
participant User as User model
participant Listing as Listing model
participant Media as deleteListingMediaAssets
participant Lead as Lead model
participant Comment as ListingComment model
participant Conversation as Conversation model
participant Outbox as NotificationOutbox
participant Notify as emitNotificationEvent
CLI->>Env: load env files
CLI->>DB: connect to MongoDB
CLI->>Job: run purge
Job->>User: find matching accounts
alt dry run
Job-->>CLI: summary only
else apply
loop each user
Job->>Listing: find anonymized listings
Job->>Media: delete listing assets
Job->>Listing: clear listing data
Job->>Lead: purge leads
Job->>Comment: mark comments purged
Job->>Conversation: delete user conversations
Job->>Notify: emit ACCOUNT_PURGED
Job->>Outbox: delete user outbox rows
Job->>User: mark account purged
end
Job-->>CLI: summary JSON
end
CLI->>DB: disconnect
El flujo empieza con la carga de variables de entorno y la conexión a MongoDB. Luego runPurgeExpiredAccounts recupera usuarios elegibles y decide entre solo contar coincidencias o aplicar cambios. En modo apply, cada usuario pasa por purgeSingleUser, que primero limpia listings y después purga el resto de datos del propietario.
Key components
{
"title": "runPurgeExpiredAccounts",
"content": "Recorre usuarios elegibles por lotes, soporta dry-run y apply, y devuelve un resumen JSON con matched, purged, errors y errorUserIds.",
"type": "function",
"filePath": "services/api/src/scripts/purgeExpiredAccounts.js",
"badges": [
"public"
]
}
{
"title": "purgeSingleUser",
"content": "Coordina la purga completa de una cuenta. Limpia listings, leads, comentarios, conversaciones, outbox y finalmente marca el usuario como purged.",
"type": "function",
"filePath": "services/api/src/scripts/purgeExpiredAccounts.js",
"badges": [
"public"
]
}
{
"title": "purgeListingsForUser",
"content": "Purga cada listing del usuario de forma independiente. Elimina assets de media, limpia campos p\u00fablicos y falla de forma agregada si alguna media no pudo borrarse.",
"type": "function",
"filePath": "services/api/src/scripts/purgeExpiredAccounts.js",
"badges": [
"public"
]
}
{
"title": "buildPurgedIdentifiers",
"content": "Genera placeholders deterministas para email y phone usando un digest derivado del _id del usuario.",
"type": "function",
"filePath": "services/api/src/scripts/purgeExpiredAccounts.js",
"badges": [
"public"
]
}
{
"title": "buildListingSanitizationSet",
"content": "Construye el conjunto de campos que se vac\u00edan en un listing durante la purga definitiva.",
"type": "function",
"filePath": "services/api/src/scripts/purgeExpiredAccounts.js",
"badges": [
"public"
]
}
{
"title": "CLI entrypoint",
"content": "Cuando el archivo se ejecuta directamente, carga dotenv, conecta a MongoDB, ejecuta la purga y escribe el resumen en JSON.",
"type": "function",
"filePath": "services/api/src/scripts/purgeExpiredAccounts.js",
"badges": [
"public"
]
}
Data & fields
Este script trabaja con varios conjuntos de datos relacionados. El usuario es la unidad principal de trabajo, pero la purga también afecta listings, leads, comentarios, conversaciones y la cola de notificaciones. La mayor parte de los campos se limpian por User.updateOne y Listing.updateOne, mientras que otras colecciones usan updateMany o deleteMany.
Execution options
[
{
"name": "dryRun",
"type": "boolean",
"description": "Cuando es true, solo cuenta coincidencias y no muta datos. El valor por defecto es true."
},
{
"name": "batchSize",
"type": "number",
"description": "Cantidad máxima de usuarios procesados por iteración. El valor por defecto es 50."
},
{
"name": "now",
"type": "Date",
"description": "Marca temporal de referencia para la elegibilidad y para los timestamps de purga. El valor por defecto es new Date()."
}
]
Summary object
[
{
"name": "mode",
"type": "string",
"required": true,
"description": "Indica dry-run o apply."
},
{
"name": "batchSize",
"type": "number",
"required": true,
"description": "Batch size efectivo usado en la ejecución."
},
{
"name": "matched",
"type": "number",
"required": true,
"description": "Cantidad de usuarios que coincidieron con el filtro."
},
{
"name": "purged",
"type": "number",
"required": true,
"description": "Cantidad de usuarios purgados con éxito."
},
{
"name": "errors",
"type": "number",
"required": true,
"description": "Cantidad de fallos registrados a nivel de usuario."
},
{
"name": "errorUserIds",
"type": "string[]",
"required": true,
"description": "Lista de IDs de usuario que fallaron durante la ejecución."
}
]
Query principal de usuarios
accountStatus[badge:required]string— Debe seranonymized_pending_purge.purgeScheduledAt[badge:required]Date— Debe ser menor o igual quenow.deletionCompletedAt[badge:required]null— Garantiza que el proceso sea idempotente.
Identificadores purgados
Genera dos valores deterministas:
email—purged+<digest>@account.invalidphone—purged-<digest>
El digest proviene de derivePurgedIdentifierDigest(userId). Los tests confirman que el digest es estable por usuario y distinto entre usuarios. También confirman que los placeholders siguen ese patrón exacto.
Campos que se escriben en User
accountStatus→purgeddeletionCompletedAt→nowemail→ placeholder deterministaphone→ placeholder deterministapasswordHash→''retentionContact→''name→Usuario eliminadofirstName→UsuariolastName→eliminadobio→''photo→''coverPhoto→''website→''location→''seo.descriptors→''seo.aiKeywords→''seo.schemaType→''
Los tests verifican que email, phone, username, passwordHash, retentionContact y accountStatus quedan en los valores esperados. También verifican que deletionCompletedAt se completa.
usernamerestorationTokenrestorationTokenExpiresAtpasswordResetTokenpasswordResetExpiresemailVerifiedAtemailVerificationTokenHashemailVerificationExpiresAtemailVerificationRequestedAtphoneVerifiedAtcontactGategoogle
El código limpia google para liberar google.sub, porque el comentario explica que existe un índice único parcial. También elimina el estado de verificación de email y teléfono.
Campos que se escriben en Listing
lifecycle.status→purgedlifecycle.purgedAt→nowmedia→[]sourceDocuments→[]meta.coverImage→''contact.name→''contact.phone→''contact.link→''contact.telegramUsername→''contact.message→''location.address→''location.postalCode→''location.latitude→nulllocation.longitude→nulllocation.mapSnapshotUrl→''location.googleMapsUrl→''requirement.searchZones→[]
Los comentarios del archivo aclaran que title, price, description, meta salvo coverImage, y las métricas se conservan. También se conserva la ubicación general como ciudad o estado, pero no la dirección exacta ni coordenadas.
Otras colecciones afectadas
Lead— Se actualizan los leads del usuario conname: '[lead purgado]',email: '',phone: '',message: '',notes: ''ydeliveryStatus: 'purged'.ListingComment— Se marcaauthorPurged: truepara que la lectura sustituya el autor por “Usuario eliminado”.Conversation— Se eliminan por completo condeleteMany({ ownerId: user._id }).NotificationOutbox— Se eliminan todas las filas del usuario, incluida la filaACCOUNT_PURGEDcreada durante la purga.
Behaviors
buildPurgedIdentifiers(userId)— Devuelve placeholders deterministas paraemailyphone, usandoderivePurgedIdentifierDigest.buildListingSanitizationSet(now)— Construye el objeto de saneamiento para listings y fijalifecycle.statusenpurged.purgeListingsForUser(userId, now)— Busca listingsanonymized, borra media de ImageKit, limpia campos públicos y actualiza referencias derelatedListingId.purgeSingleUser(user, now)— Ejecuta la purga completa de una cuenta en orden estricto.runPurgeExpiredAccounts(options)— Procesa usuarios elegibles por lotes, soportadryRuny devuelve un resumen JSON.- Bloque CLI — Detecta ejecución directa, carga
.env, resuelveMONGODB_URI, conecta a MongoDB, ejecuta el runner y desconecta al final.
La función purgeListingsForUser tiene una regla importante: si falla la eliminación de media de un listing, el listing no se marca como purged. El código acumula los IDs fallidos, lanza un error agregado y evita que purgeSingleUser continúe.
purgeSingleUser ejecuta luego estas acciones, en este orden:
- purga listings;
- purga leads;
- marca comentarios como purgados;
- elimina conversaciones;
- emite
ACCOUNT_PURGED; - borra outbox del usuario;
- actualiza el propio
User.
runPurgeExpiredAccounts maneja el paginado de forma distinta según el modo:
- en
dryRun, usaskip; - en
apply, excluye los IDs fallidos con$nin; - en ambos casos, incrementa
matched; - en
apply, incrementapurgedsolo después depurgeSingleUser.
Workflows
1. Detectar ejecución directa | El script compara `process.argv[1]` con su propia URL de archivo.
2. Cargar configuración | Primero carga `../../.env` y después `dotenv.config()` con el entorno actual.
3. Resolver la conexión | Usa `MONGODB_URI` o el fallback `mongodb://127.0.0.1:27017/plano_backoffice`.
4. Elegir modo | `--apply` activa escrituras; `--dry-run` o la ausencia de `--apply` mantienen el modo de solo lectura.
5. Ejecutar la purga | `runPurgeExpiredAccounts` procesa usuarios por lotes y devuelve un resumen JSON.
6. Cerrar la conexión | El proceso desconecta MongoDB en un bloque `finally`.
Ejemplos de uso directo:
node services/api/src/scripts/purgeExpiredAccounts.js
node services/api/src/scripts/purgeExpiredAccounts.js --dry-run
node services/api/src/scripts/purgeExpiredAccounts.js --apply --batch-size=100
El bloque CLI imprime el resumen con JSON.stringify(summary, null, 2). Si ocurre un fallo fatal, escribe [purgeExpiredAccounts] fatal: y deja process.exitCode = 1.
Gotchas & edge cases
[!WARNING]
dryRunes el comportamiento por defecto. Si no se pasa--apply, el script no escribe nada.
[!CAUTION] Si falla la eliminación de media de un listing, la purga de ese usuario queda incompleta. El usuario permanece en
anonymized_pending_purgey se reintenta en una corrida futura.
[!IMPORTANT]
NotificationOutbox.deleteMany({ recipientUserId })elimina también la fila recién creada paraACCOUNT_PURGED. El comentario del código dice que ese historial ya cumplió su propósito operativo.
[!NOTE] El
User.updateOnesolo se ejecuta si el documento sigue enaccountStatus: 'anonymized_pending_purge'. Eso evita tocar cuentas que ya cambiaron de estado.
[!NOTE] El script no reemplaza
usernamecon un placeholder. Lo elimina con$unset, porque el comentario indica que su índice parcial lo permite.
Related
Este archivo usa varios módulos del backend ya existentes. Cada relación está explícita en los imports del archivo.
services/api/src/models/User.js— Fuente del modeloUser.services/api/src/models/Listing.js— Fuente del modeloListing.services/api/src/models/ListingComment.js— Fuente del modeloListingComment.services/api/src/models/Lead.js— Fuente del modeloLead.services/api/src/models/Conversation.js— Fuente del modeloConversation.services/api/src/models/NotificationOutbox.js— Fuente del modeloNotificationOutbox.services/api/src/utils/encryption.js— ProveederivePurgedIdentifierDigest.services/api/src/modules/media/media.service.js— ProveedeleteListingMediaAssets.services/api/src/modules/notifications/notification.events.js— ProveeEVENT_TYPESyemitNotificationEvent.
También existe contexto de scripts hermanos. Los tests del repositorio muestran que los jobs de ciclo de vida exportan su runner, aceptan --dry-run, --apply y --batch-size, y solo conectan a la base cuando se ejecutan directamente.
Además, el repositorio contiene un script histórico para NotificationOutbox. Ese script aclara que purgeExpiredAccounts.js ya limpia outbox para purgas nuevas, mientras que el script histórico cubre cuentas ya purgadas en corridas anteriores.