services/api/src/scripts/inventoryDeletionState.js

Overview

Este archivo define una CLI de inventario administrativo de solo lectura para el estado de eliminaciones de Requirements bajo D-09. Su objetivo es clasificar registros, no recuperarlos, y emitir resultados en jsonl o en un resumen textual. El script se expone como inventory:deletions en package.json y está cubierto por pruebas que verifican que no escribe en MongoDB, no usa $merge ni $out, y no importa dependencias prohibidas

[!IMPORTANT] La ejecución está diseñada para fallar cerrada. Solo acepta una base local autorizada, validada contra INVENTORY_ALLOWED_DB, y rechaza opciones desconocidas con código 64. También rechaza destinos no permitidos con código 65

[!NOTE] El script no lee .env. La URI se pasa manualmente en INVENTORY_MONGODB_URI. Además, fija autoIndex:false y autoCreate:false antes de cargar modelos, y conecta con directConnection:true

How it works

flowchart TD
    Start[main]
    Parse[parseInventoryArgs]
    Usage[io error uso]
    Target[assertInventoryTarget]
    Refused[io error destino]
    Safe[mongoose safe flags]
    Import[runDeletionInventory import]
    Connect[mongoose connect]
    ConnFail[io error conexion]
    Hello[hello command]
    LocalCheck[assertConnectedServerIsLocal]
    LocalRefused[io error destino]
    Pseudonym[createPseudonymizer]
    Run[runDeletionInventory]
    Summary[summaryText]
    Record[formatRecord]
    Write[io write]
    Finish[mongoose disconnect]

    Start --> Parse
    Parse -->|invalid args| Usage
    Parse --> Target
    Target -->|rejected| Refused
    Target --> Safe
    Safe --> Import
    Import --> Connect
    Connect -->|fail| ConnFail
    Connect --> Hello
    Hello --> LocalCheck
    LocalCheck -->|rejected| LocalRefused
    LocalCheck --> Pseudonym
    Pseudonym --> Run
    Run -->|summary item| Summary
    Run -->|regular item| Record
    Summary --> Write
    Record --> Write
    Write --> Finish

La secuencia principal valida argumentos, valida el destino, desactiva la creación automática de índices y colecciones, conecta a MongoDB y verifica que el servidor reportado también sea local. Después, ejecuta runDeletionInventory y formatea cada salida según jsonl o summary

Key components

{
    "title": "UsageError",
    "content": "Clase de error usada para rechazar opciones de CLI no permitidas o inv\u00e1lidas.",
    "type": "class",
    "filePath": "services/api/src/scripts/inventoryDeletionState.js",
    "badges": [
        "public"
    ]
}
{
    "title": "parseInventoryArgs",
    "content": "[badge:public] Parsea `argv`, acepta solo las opciones permitidas y valida secci\u00f3n, lote, l\u00edmite y formato.",
    "type": "function",
    "filePath": "services/api/src/scripts/inventoryDeletionState.js",
    "badges": [
        "public"
    ]
}
{
    "title": "assertInventoryTarget",
    "content": "[badge:public] Envuelve `assertLocalMongoTarget` y aplica la base permitida desde `INVENTORY_ALLOWED_DB`.",
    "type": "function",
    "filePath": "services/api/src/scripts/inventoryDeletionState.js",
    "badges": [
        "public"
    ]
}
{
    "title": "createPseudonymizer",
    "content": "[badge:public] Crea un pseudonimizador HMAC SHA-256 estable dentro de una corrida o con una clave dada.",
    "type": "function",
    "filePath": "services/api/src/scripts/inventoryDeletionState.js",
    "badges": [
        "public"
    ]
}
{
    "title": "formatRecord",
    "content": "[badge:public] Aplica la pol\u00edtica de pseudonimizaci\u00f3n y preserva sin cambios los registros `summary` y `error`.",
    "type": "function",
    "filePath": "services/api/src/scripts/inventoryDeletionState.js",
    "badges": [
        "public"
    ]
}
{
    "title": "summaryText",
    "content": "Convierte el resumen estructurado del inventario en texto legible para salida `summary`.",
    "type": "function",
    "filePath": "services/api/src/scripts/inventoryDeletionState.js",
    "badges": [
        "public"
    ]
}
{
    "title": "main",
    "content": "[badge:public] Punto de entrada testeable. Coordina validaci\u00f3n, conexi\u00f3n, ejecuci\u00f3n del inventario, salida y c\u00f3digo final.",
    "type": "function",
    "filePath": "services/api/src/scripts/inventoryDeletionState.js",
    "badges": [
        "public"
    ]
}

Data & fields

Environment inputs

[
    {
        "name": "INVENTORY_MONGODB_URI",
        "type": "string",
        "required": true,
        "description": "URI de MongoDB que el CLI recibe manualmente."
    },
    {
        "name": "INVENTORY_ALLOWED_DB",
        "type": "string",
        "required": true,
        "description": "Nombre de base autorizado para `assertInventoryTarget`."
    },
    {
        "name": "INVENTORY_PSEUDONYM_KEY",
        "type": "string",
        "description": "Clave opcional para generar pseudónimos estables dentro de una corrida."
    }
]

CLI options

[
    {
        "name": "--section",
        "type": "string",
        "description": "Lista separada por comas. Acepta `requirements`, `claims`, `conversations` y `shared`."
    },
    {
        "name": "--batch-size",
        "type": "number",
        "description": "Tamaño de lote entre 1 y 1000. El valor por defecto es 200."
    },
    {
        "name": "--limit",
        "type": "number",
        "description": "Límite total de elementos a procesar. El valor por defecto es 10000."
    },
    {
        "name": "--format",
        "type": "string",
        "description": "Formato de salida. Solo acepta `jsonl` o `summary`."
    },
    {
        "name": "--share",
        "type": "boolean",
        "description": "Activa la pseudonimización adicional de identificadores de listings y conversations."
    }
]

Output bootstrap in jsonl

[
    {
        "name": "kind",
        "type": "string",
        "required": true,
        "description": "Siempre `run` en la primera línea de salida JSONL."
    },
    {
        "name": "runId",
        "type": "string",
        "required": true,
        "description": "UUID de la corrida, añadido a cada línea emitida."
    },
    {
        "name": "schemaVersion",
        "type": "number",
        "required": true,
        "description": "Versión de esquema fija. El código usa `1`."
    },
    {
        "name": "sections",
        "type": "string[]",
        "required": true,
        "description": "Secciones elegidas para el inventario."
    },
    {
        "name": "share",
        "type": "boolean",
        "required": true,
        "description": "Indica si se activó la pseudonimización ampliada."
    },
    {
        "name": "readOnly",
        "type": "boolean",
        "required": true,
        "description": "Marca la corrida como lectura בלבד."
    }
]
[
    {
        "name": "observedAt",
        "type": "string",
        "required": true,
        "description": "Marca temporal que `summaryText` imprime como cabecera."
    },
    {
        "name": "failed",
        "type": "boolean",
        "required": true,
        "description": "Se refleja en la cabecera como `CON ERRORES` si es verdadero."
    },
    {
        "name": "sections",
        "type": "object",
        "required": true,
        "description": "Mapa por sección inventariada.",
        "children": [
            {
                "name": "scanned",
                "type": "number",
                "required": true,
                "description": "Cantidad revisada."
            },
            {
                "name": "emitted",
                "type": "number",
                "required": true,
                "description": "Cantidad emitida."
            },
            {
                "name": "errors",
                "type": "number",
                "required": true,
                "description": "Cantidad de errores observados."
            },
            {
                "name": "limitReached",
                "type": "boolean",
                "required": true,
                "description": "Indica si se alcanzó el límite de trabajo."
            },
            {
                "name": "classifications",
                "type": "object",
                "required": true,
                "description": "Conteo por clasificación observada."
            }
        ]
    }
]

Record shaping rules

  • summary y error se devuelven sin cambios por formatRecord.
  • related.ownerId se transforma en related.ownerRef.
  • media_claim y shared_file siempre usan pseudónimos de tipo file.
  • Con --share, también se pseudonimizan id, related.listingId, related.requirementId y related.listingIds.
  • Sin --share, solo se pseudonimiza ownerId y los tipos de archivo mencionados.
  • El pseudónimo se genera con HMAC-SHA256 y se recorta a 16 hex caracteres.

[!NOTE] El pseudonimizador devuelve null si el valor original es null o undefined. Eso evita convertir ausencias en cadenas falsas.

Behaviors

  • EXIT — expone los códigos OK: 0, CONNECTION: 1, INVENTORY_ERRORS: 2, USAGE: 64 y TARGET_REFUSED: 65.
  • UsageError — representa errores de uso de la CLI.
  • parseInventoryArgs — acepta solo --section, --batch-size, --limit, --format y --share.
  • parseInventoryArgs — rechaza cualquier otra opción con UsageError.
  • assertInventoryTarget — valida que la URI apunte a una base local permitida.
  • assertConnectedServerIsLocal — se reexporta desde ./lib/localMongoTarget.js.
  • createPseudonymizer — construye un closure de pseudonimización por corrida.
  • formatRecord — aplica la política de ocultación y reescritura de identificadores.
  • summaryText — genera un resumen plano por secciones.
  • main — ejecuta la corrida completa y devuelve el código final.
  • main — recibe io inyectable, lo que facilita pruebas deterministas.
  • main — desconecta siempre en finally.

Workflows

1. Define el destino | Establece `INVENTORY_MONGODB_URI` y `INVENTORY_ALLOWED_DB`.
2. Elige el alcance | Pasa `--section`, `--batch-size`, `--limit`, `--format` o `--share`.
3. Ejecuta la CLI | Usa `node src/scripts/inventoryDeletionState.js` o `npm run inventory:deletions`.
4. Lee la salida | Consume líneas `jsonl` o el resumen textual y revisa el código de salida.

La secuencia real usa mongoose.connect con directConnection:true, verifica el hello del servidor, y luego consume runDeletionInventory. El script termina con process.exitCode cuando se ejecuta como módulo principal

Gotchas & edge cases

[!WARNING] No existe ninguna opción de escritura. Cualquier opción desconocida, incluida cualquier forma de apply, se rechaza con código 64. El inventario también puede devolver 65 si el destino no pasa la validación local

[!CAUTION] La validación local no demuestra que la base sea de pruebas. Un túnel local hacia otra base se ve igual desde esta guardia. El código lo reconoce explícitamente en sus comentarios operativos.

[!NOTE] El script no importa servicios de eliminación ni ImageKit. Las pruebas estáticas confirman que tampoco usa métodos de escritura, $merge o $out en este flujo

[
    {
        "title": "0 OK",
        "body": "La corrida terminó sin errores de conexión ni de inventario."
    },
    {
        "title": "1 Connection",
        "body": "Falló `mongoose.connect` o la conexión no pudo establecerse."
    },
    {
        "title": "2 Inventory errors",
        "body": "El inventario se interrumpió o emitió errores durante la ejecución."
    },
    {
        "title": "64 Usage",
        "body": "La CLI recibió una opción no permitida o un valor inválido."
    },
    {
        "title": "65 Target refused",
        "body": "La URI o el servidor conectado no cumplieron las reglas de destino local."
    }
]

Este archivo se conecta con services/api/src/scripts/lib/localMongoTarget.js, que aporta las guardas de destino local y el reexport de assertConnectedServerIsLocal. También depende de services/api/src/modules/reconciliation/deletion-inventory.service.js, que entrega el iterador runDeletionInventory consumido por la CLI

Las pruebas de apoyo están en services/api/test/deletion-inventory-classify.test.js, donde se importan parseInventoryArgs, assertInventoryTarget, createPseudonymizer, formatRecord y main. La validación de no escritura y de imports prohibidos aparece en services/api/test/e6-1-inventory-static.test.js

La exposición operativa también aparece en package.json como inventory:deletions. Esa entrada apunta directamente a este script, sin envoltorios adicionales