services/api/src/scripts/auditOrphanListings.js
Overview
Este script audita Listing huérfanos heredados de seed.js y separa tres casos: valid, seed_orphan y ambiguous_orphan. La lógica usa evidencias concretas de origen histórico, como admin_users, títulos exactos de fixtures y huellas de media de picsum.photos/seed/ .
También incluye rutas de limpieza controlada. Por defecto corre en modo lectura, y sus borrados se limitan a listas explícitas de _id ya clasificadas en la misma ejecución. El archivo deja claro que no forma parte del Account Closure Lifecycle y no toca User.accountStatus ni Listing.lifecycle.status .
[!IMPORTANT] Este script es una herramienta manual de integridad y migración. No es un job recurrente, ni usa cron, ni se ejecuta al arrancar la API .
How it works
flowchart TD
Start[Direct run]
Env[Load env and connect Mongo]
Audit[runAuditOrphanListings]
Classify[classifyOrphanListing]
Summary[printSummary]
SeedPlan[buildApprovedSeedOrphanCleanupPlan]
OwnerlessPlan[buildOwnerlessAdministrativeCleanupPlan]
SeedApply[applySeedOrphanCleanup]
RelationApply[applyApprovedSeedOrphanCleanup]
OwnerlessApply[applyOwnerlessAdministrativeCleanup]
Media[deleteListingMediaAssets]
Start --> Env
Env --> Audit
Audit --> Classify
Audit --> Summary
Audit --> SeedPlan
Audit --> OwnerlessPlan
SeedPlan --> SeedApply
SeedPlan --> RelationApply
SeedApply --> Media
RelationApply --> Media
OwnerlessPlan --> OwnerlessApply
OwnerlessApply --> Media
El flujo directo carga variables de entorno, conecta mongoose, ejecuta la auditoría y calcula planes de limpieza. Después imprime el resumen y aplica cambios solo si el operador activa --apply o --apply-ownerless .
La auditoría lee Listing, User, ListingLike, ListingComment, ListingEvent, Lead y Campaign, y además consulta la colección legacy admin_users. La clasificación conserva una postura conservadora: si un createdBy resuelve a un User real, el Listing se considera válido y se excluye del detalle .
Key components
{
"title": "classifyOrphanListing",
"content": "Clasifica un Listing ya hu\u00e9rfano como seed_orphan o ambiguous_orphan. Usa admin_users, t\u00edtulos conocidos y media con la huella picsum.photos/seed/.",
"type": "function",
"filePath": "services/api/src/scripts/auditOrphanListings.js",
"badges": [
"public"
]
}
{
"title": "runAuditOrphanListings",
"content": "Ejecuta la auditor\u00eda completa. Carga Listings y Users, clasifica hu\u00e9rfanos, y opcionalmente calcula relaciones.",
"type": "function",
"filePath": "services/api/src/scripts/auditOrphanListings.js",
"badges": [
"public"
]
}
{
"title": "buildApprovedSeedOrphanCleanupPlan",
"content": "Construye el plan relation-aware para seed_orphan. Separa candidatos aprobados y excluidos, y aplica guardas sobre usuarios vigentes y ImageKit compartido.",
"type": "function",
"filePath": "services/api/src/scripts/auditOrphanListings.js",
"badges": [
"public"
]
}
{
"title": "buildOwnerlessAdministrativeCleanupPlan",
"content": "Construye una limpieza administrativa separada para ambiguous_orphan sin ownership recuperable. Devuelve candidatos seguros y casos para revisi\u00f3n manual.",
"type": "function",
"filePath": "services/api/src/scripts/auditOrphanListings.js",
"badges": [
"public"
]
}
Data & fields
Constants
KNOWN_SEED_LISTING_TITLES[badge:read-only]Set<string>— Títulos exactos reconocidos como fixtures históricos deseed.js.SEED_MEDIA_PATTERN[badge:read-only]RegExp— Patrónpicsum.photos/seed/usado como huella de media de seed.PREVIOUSLY_OBSERVED_SAFE_COUNT[badge:read-only]number— Conteo histórico informativo para comparar corridas, sin efecto de control.
Resultado de auditoría
[
{
"name": "totals",
"type": "object",
"required": true,
"description": "Conteos globales de la corrida.",
"children": [
{
"name": "total",
"type": "number",
"required": true,
"description": "Total de Listings cargados desde Mongo."
},
{
"name": "valid",
"type": "number",
"required": true,
"description": "Listings cuyo createdBy resuelve a un User real."
},
{
"name": "orphan",
"type": "number",
"required": true,
"description": "Listings sin User válido."
},
{
"name": "seed_orphan",
"type": "number",
"required": true,
"description": "Huérfanos con evidencia directa de seed.js."
},
{
"name": "ambiguous_orphan",
"type": "number",
"required": true,
"description": "Huérfanos sin evidencia suficiente de seed.js."
}
]
},
{
"name": "rows",
"type": "array",
"required": true,
"description": "Detalle de cada Listing huérfano clasificado.",
"children": [
{
"name": "_id",
"type": "string",
"required": true,
"description": "Identificador del Listing."
},
{
"name": "title",
"type": "string",
"required": true,
"description": "Título del Listing."
},
{
"name": "intent",
"type": "string",
"required": true,
"description": "requirement si el intent original lo indica; offer en otros casos."
},
{
"name": "publicationStatus",
"type": "string",
"required": true,
"description": "publication.status o draft si falta."
},
{
"name": "lifecycleStatus",
"type": "string",
"required": true,
"description": "lifecycle.status o missing si no existe."
},
{
"name": "createdBy",
"type": "string|null",
"required": true,
"description": "Valor crudo de ownership almacenado en el Listing."
},
{
"name": "ownerResolved",
"type": "boolean",
"required": true,
"description": "Siempre false en las filas huérfanas reportadas."
},
{
"name": "ownerSnapshot",
"type": "string",
"required": true,
"description": "contact.name capturado en la auditoría."
},
{
"name": "createdAt",
"type": "date|null",
"required": true,
"description": "Fecha de creación del documento."
},
{
"name": "classification",
"type": "string",
"required": true,
"description": "seed_orphan o ambiguous_orphan."
},
{
"name": "reasons",
"type": "array",
"required": true,
"description": "Evidencias que justifican seed_orphan."
},
{
"name": "notes",
"type": "array",
"required": true,
"description": "Notas informativas para ambiguous_orphan."
},
{
"name": "relations",
"type": "object",
"description": "Conteos de dependencias cuando includeRelations está activo.",
"children": [
{
"name": "likes",
"type": "number",
"required": true,
"description": "Cantidad de ListingLike."
},
{
"name": "comments",
"type": "number",
"required": true,
"description": "Cantidad de ListingComment."
},
{
"name": "events",
"type": "number",
"required": true,
"description": "Cantidad de ListingEvent."
},
{
"name": "leads",
"type": "number",
"required": true,
"description": "Cantidad de Lead."
},
{
"name": "referrals",
"type": "number",
"required": true,
"description": "Listings que apuntan al relatedListingId."
},
{
"name": "campaigns",
"type": "number",
"required": true,
"description": "Campaigns que apuntan al Listing."
}
]
}
]
}
]
Plan de limpieza seed_orphan
[
{
"name": "_id",
"type": "string",
"required": true,
"description": "Identificador del Listing aprobado para limpieza."
},
{
"name": "title",
"type": "string",
"required": true,
"description": "Título del Listing."
},
{
"name": "likesToDelete",
"type": "array",
"required": true,
"description": "IDs exactos de ListingLike a borrar."
},
{
"name": "commentsToDelete",
"type": "array",
"required": true,
"description": "IDs exactos de ListingComment a borrar."
},
{
"name": "eventsToDelete",
"type": "array",
"required": true,
"description": "IDs exactos de ListingEvent a borrar."
},
{
"name": "leadsToDelete",
"type": "array",
"required": true,
"description": "IDs exactos de Lead a borrar."
},
{
"name": "mediaCleanupRequired",
"type": "boolean",
"required": true,
"description": "Indica si la media contiene assets reales de ImageKit."
},
{
"name": "explicitlyApproved",
"type": "boolean",
"required": true,
"description": "Marca holdouts aprobados manualmente en la misma corrida."
},
{
"name": "reason",
"type": "string",
"required": true,
"description": "Justificación operativa del lugar en el plan."
}
]
Plan administrativo ambiguous_orphan
[
{
"name": "_id",
"type": "string",
"required": true,
"description": "Identificador del Listing."
},
{
"name": "title",
"type": "string",
"required": true,
"description": "Título del Listing."
},
{
"name": "mediaCleanupRequired",
"type": "boolean",
"required": true,
"description": "Indica si requiere limpieza de media al borrar."
},
{
"name": "decision",
"type": "string",
"required": true,
"description": "safe_to_delete_ownerless cuando pasa todas las guardas."
},
{
"name": "reason",
"type": "string",
"required": true,
"description": "Motivo de la decisión administrativa."
}
]
Behaviors
normalizeTitle(title)— Convierte el título a cadena y recorta espacios.hasSeedMediaFingerprint(media)— Detecta media conpicsum.photos/seed/.classifyOrphanListing(listing, { adminUserEmailById })— Devuelveseed_orphanoambiguous_orphan, más razones o notas.loadAdminUserEmailById()— Lee la colecciónadmin_usersy construye unMappor_id.countRelations(listingId)— Cuentalikes,comments,events,leads,referralsycampaigns.runAuditOrphanListings({ includeRelations = true })— Carga Listings y Users, clasifica orphans y, opcionalmente, adjunta relaciones.hasNoRelations(row)— Verifica que todas las relaciones cuenten cero.computeSafeSeedOrphanIds(rows)— Extrae solo_iddeseed_orphancon cero relaciones.printSummary(result)— Imprime totales, candidatos seguros y filas clasificadas.applySeedOrphanCleanup(safeIds)— Limpia media, borra Listings por_idexacto y reporta fallos de media.loadRelationRecords(listingId)— Carga documentos relacionados por Listing.collectRelationIdentityIds(records)— Reúne identidades referenciadas por likes, comments, events y leads.hasImageKitMedia(media)— Detecta media real de ImageKit porprovider.findSharedImageKitFileIds(listingId, media)— Busca fileIds de ImageKit compartidos con otros Listings.buildApprovedSeedOrphanCleanupPlan(rows, options)— Construye el plan relation-aware y excluye casos con usuarios vigentes, emails vigentes o ImageKit compartido.applyApprovedSeedOrphanCleanup(plan)— Revalida estado, borra relaciones aprobadas, limpia media y elimina el Listing.buildOwnerlessAdministrativeCleanupPlan(rows)— Aíslaambiguous_orphancon ownership nulo y sin relaciones ni media compartida.applyOwnerlessAdministrativeCleanup(plan)— Revalida estado y borra Listings ownerless seguros.printOwnerlessCleanupPlan(result)— Imprime el plan administrativo y sus exclusiones.printCleanupPlan(result)— Imprime el plan relation-aware y sus exclusiones.
Workflows
1. Cargar entorno | El bloque directo ejecuta `dotenv.config` y abre `mongoose` contra `MONGODB_URI` o la base local por defecto.
2. Auditar datos | `runAuditOrphanListings` carga Listings, Users y la colección legacy `admin_users`.
3. Clasificar huérfanos | `classifyOrphanListing` decide entre `seed_orphan` y `ambiguous_orphan`.
4. Construir planes | `buildApprovedSeedOrphanCleanupPlan` y `buildOwnerlessAdministrativeCleanupPlan` separan candidatos y exclusiones.
5. Ejecutar en modo lectura | Sin flags destructivos, el script imprime el resumen y termina sin modificar datos.
6. Aplicar limpieza seed | `--apply` borra solo `seed_orphan` con cero relaciones en esa misma corrida.
7. Aplicar limpieza relation-aware | `--apply` también ejecuta el plan aprobado con guardas adicionales.
8. Aplicar limpieza ownerless | `--apply-ownerless` ejecuta la ruta separada para `ambiguous_orphan`.
Gotchas & edge cases
[!WARNING]
classifyOrphanListingno clasifica por palabras comopruebaotestsolas. Solo añade una nota informativa. El código evita tratar ese título como prueba de origenseed.js
[!IMPORTANT]
--applysolo borraseed_orphancon cero relaciones en la misma corrida.ambiguous_orphanqueda fuera y requiere--apply-ownerless
[!CAUTION] Antes de cada borrado, los flujos de apply revalidan el estado actual. Si el Listing ya no es huérfano, el script lo omite
[!WARNING] Si una media de ImageKit está compartida con otro Listing, el candidato se excluye. Ni la aprobación explícita lo habilita para borrado
[!NOTE]
PREVIOUSLY_OBSERVED_SAFE_COUNTsolo informa un cambio entre corridas. No bloquea la ejecución y no decide borrados
[!TIP]
includeRelationspuede desactivarse enrunAuditOrphanListingspara una pasada rápida. En ese caso,rowsno incluyerelations
Related
Este script se relaciona directamente con los modelos Listing, User, ListingLike, ListingComment, ListingEvent, Lead y Campaign. También depende de deleteListingMediaAssets en services/api/src/modules/media/media.service.js para limpiar media asociada antes del borrado físico .
Su comportamiento se apoya en la colección legacy admin_users, usada solo como fuente de evidencia histórica. La intención general es cerrar deuda documental y de integridad, no redefinir el ciclo de vida operativo de cuentas o Listings .