Files
seguidorDeLotes/nuxt4/server/database
josedario87 f3a170c882
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 1m6s
Feat: agregar 4 flujos complejos al seed y filtrar lotes finales en grafos
- Agregar flujo 5: Las Nubes (Geisha) + El Paraíso (Castillo) → SEC-EXOTIC-001
- Agregar flujo 6: Santa Rita (Caturra Rojo) → División → SEC-SRT-PREM-001 + SEC-SRT-STD-001
- Agregar flujo 7: Trinidad + San José + Villa Rosa → Triple Blend → SEC-TRIPLE-001
- Agregar flujo 8: Mezcla de segundas calidades → SEC-COMERCIAL-001
- Implementar filtro soloFinales en queries, API y composable
- Modificar tab Grafos para mostrar solo lotes finales (sin hijos)
- Actualizar descripción de tab Grafos para clarificar el filtro
- Total: 7 lotes finales de secado para visualización de grafos
2025-11-22 04:05:24 -06:00
..
2025-11-22 01:10:37 -06:00

Database - Documentación Completa

Este directorio contiene los scripts SQL para inicializar y gestionar la base de datos PostgreSQL del sistema de trazabilidad.

Tabla de Contenidos


Archivos SQL

01_schema.sql

Crea el esquema completo de la base de datos:

  • Tablas: lotes, operaciones, operacion_lotes
  • Índices para optimización
  • Función recursiva get_trazabilidad() para queries de trazabilidad completa
  • Vista vista_lotes_con_origen
  • Constraints y validaciones
  • Idempotente: Usa CREATE TABLE IF NOT EXISTS

02_seed.sql

Datos de ejemplo que representan un flujo completo de trazabilidad:

  • Ingreso de uva (2086 kg)
  • Despulpado → primera, segunda, rechazos
  • Oreado (con error de registro intencional)
  • Ajuste de merma (1500 → 1480 kg)
  • Ajuste de tipo (oreado → presecado)
  • Reposo
  • Secado final (mezcla de 2 lotes = 2000 kg)
  • Total: 10 lotes, 7 operaciones, 16 relaciones
  • Idempotente con TRUNCATE: Limpia datos antes de insertar

⚠️ Peculiaridades de Implementación

Esta sección documenta los desafíos técnicos encontrados y las soluciones implementadas durante el desarrollo.

Conexión de la app

  • Se usa una cadena de conexión única (NUXT_POSTGRES_URL) consumida desde runtimeConfig.postgresUrl.
  • Formato recomendado: postgres://seguidor:seguidor_password@postgres:5432/seguidor_lotes.
  • El driver pg soporta scram-sha-256 (default de Postgres 16), no se toca pg_hba.conf.

Inicialización de la Base de Datos

📂 Scripts en /docker-entrypoint-initdb.d/

Los scripts se ejecutan solo si el volumen de datos está vacío (primera vez):

  1. 01_schema.sql - Crea estructura (tablas, funciones, vistas)
  2. 02_seed.sql - Inserta datos de ejemplo

Orden de ejecución: Los archivos se ejecutan en orden alfabético/numérico.

🔄 Idempotencia de Scripts

01_schema.sql - Totalmente Idempotente
CREATE TABLE IF NOT EXISTS lotes (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  -- ...
);

CREATE INDEX IF NOT EXISTS idx_lotes_tipo ON lotes(tipo);
  • Puede ejecutarse múltiples veces sin errores
  • No duplica tablas ni índices
  • Seguro para re-ejecución
02_seed.sql - Idempotente con TRUNCATE ⚠️
-- Línea 17: Limpia datos antes de insertar
TRUNCATE TABLE operacion_lotes, operaciones, lotes CASCADE;

-- Luego inserta datos de ejemplo
INSERT INTO lotes (codigo, tipo, ...) VALUES (...);

IMPORTANTE:

  • ⚠️ Si las tablas no existen, TRUNCATE falla
  • Por eso el endpoint seed-database ejecuta primero el schema
  • Puede ejecutarse múltiples veces si las tablas existen

🔍 Verificación en Workflow de Gitea Actions

El workflow verifica si es necesario inicializar:

TABLE_EXISTS=$(docker exec lotes-postgres psql -U seguidor -d seguidor_lotes \
  -tAc "SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_name = 'lotes');")

if [ "$TABLE_EXISTS" = "f" ]; then
  echo "📝 Ejecutando scripts de inicialización..."
  docker exec -i lotes-postgres psql -U seguidor -d seguidor_lotes < nuxt4/server/database/01_schema.sql
  docker exec -i lotes-postgres psql -U seguidor -d seguidor_lotes < nuxt4/server/database/02_seed.sql
fi

Comportamiento:

  • Primera vez: Ejecuta schema + seed
  • Redeploys: Solo actualiza contenedor de app, datos persisten
  • Después de docker-compose down -v: Reinicializa todo

Persistencia de Datos

💾 Volumen Docker

# docker-compose.yml
volumes:
  postgres_data:  # Volumen nombrado

services:
  postgres:
    volumes:
      - postgres_data:/var/lib/postgresql/data

Comportamiento:

  • Datos persisten entre docker-compose down y docker-compose up
  • Datos persisten entre redespliegues del workflow
  • Datos se pierden con docker-compose down -v (elimina volúmenes)

🔄 Resetear a Estado Inicial

Opción 1: Eliminar volumen (desde servidor)

docker-compose --project-name lotes down -v
# Próximo deploy reinicializará automáticamente

Opción 2: Usar botones de debug (desde web)

  1. Click en "🗑️ BORRAR TODA LA BD" → Elimina todas las tablas
  2. Click en "🌱 CARGAR DATOS DE EJEMPLO" → Recrea schema + seed
  3. Recarga la página

Opción 3: Trigger redeploy después de borrar

# 1. Click "🗑️ BORRAR TODA LA BD" en la web
# 2. Trigger redeploy
git commit --allow-empty -m "Trigger reinit DB" && git push
# El workflow detectará tablas faltantes y ejecutará schema + seed

Conexión desde Node.js

🔌 Pool de Conexiones

Configuración en server/utils/db.ts:

const config = useRuntimeConfig()
const pool = new Pool({
  connectionString: config.postgresUrl, // Ej: postgres://user:pass@host:5432/db
  max: 10,
  idleTimeoutMillis: 30000,
  connectionTimeoutMillis: 10000,
})

Peculiaridades:

  1. Cadena única: evita discrepancias entre host/puerto/usuario/contraseña.
  2. Logs solo en desarrollo:
    if (process.env.NODE_ENV !== 'production') {
      pool.on('connect', () => {
        console.log('Nueva conexión establecida con PostgreSQL')
      })
    }
    

Race Condition: App vs PostgreSQL

Aunque docker-compose tiene depends_on con service_healthy, el healthcheck solo verifica que PostgreSQL responde. Si necesitas máxima robustez puedes añadir un pequeño retry en la app o un pg_isready antes de exponer el servicio, pero con la cadena de conexión única y el pool el arranque es estable.


Endpoints de Debug

⚠️ Endpoints Temporales

Estos endpoints están marcados como TEMPORALES y NO DEBEN ELIMINARSE sin consultar a Dario/Draganel/nucleo000.

POST /api/debug/reset-database

Función: Elimina completamente todas las tablas de la base de datos.

Implementación:

await client.query('DROP TABLE IF EXISTS operacion_lotes CASCADE')
await client.query('DROP TABLE IF EXISTS operaciones CASCADE')
await client.query('DROP TABLE IF EXISTS lotes CASCADE')
await client.query('DROP FUNCTION IF EXISTS get_trazabilidad CASCADE')
await client.query('DROP VIEW IF EXISTS vista_lotes_con_origen CASCADE')

Notas:

  • Usa DROP TABLE (no TRUNCATE) para que el workflow detecte la ausencia de tablas
  • Después de usar, el próximo deploy reinicializará automáticamente
  • Requiere confirmación del usuario en el frontend

POST /api/debug/seed-database

Función: Recrea schema y carga datos de ejemplo.

Implementación:

// 1. Ejecutar schema (crea tablas si no existen)
const schemaSQL = await readFile('server/database/01_schema.sql', 'utf-8')
await client.query(schemaSQL)

// 2. Ejecutar seed (truncate + insert)
const seedSQL = await readFile('server/database/02_seed.sql', 'utf-8')
await client.query(seedSQL)

Peculiaridad: Los archivos SQL deben estar en el contenedor de la app

Dockerfile:

# Copy SQL files for seed endpoint
COPY --from=builder /app/server/database /app/server/database

Sin esta línea, el endpoint devuelve ENOENT: no such file or directory.


Ejecución de Scripts

Automática (Docker Compose)

# docker-compose.yml
volumes:
  - ./nuxt4/server/database:/docker-entrypoint-initdb.d:ro

PostgreSQL ejecuta todos los archivos .sql y .sh en orden alfabético dentro de /docker-entrypoint-initdb.d/ solo la primera vez.

Manual - Desde el contenedor Docker

# Conectarse al contenedor
docker exec -it lotes-postgres psql -U seguidor -d seguidor_lotes

# Dentro de psql, ejecutar:
\i /docker-entrypoint-initdb.d/01_schema.sql
\i /docker-entrypoint-initdb.d/02_seed.sql

Manual - Desde tu máquina local

# Asegúrate de tener psql instalado
psql -h localhost -U seguidor -d seguidor_lotes -f 01_schema.sql
psql -h localhost -U seguidor -d seguidor_lotes -f 02_seed.sql

Manual - Vía API (endpoints de debug)

# Resetear BD
curl -X POST https://lotes.nucleoriofrio.com/api/debug/reset-database

# Cargar datos de ejemplo
curl -X POST https://lotes.nucleoriofrio.com/api/debug/seed-database

Troubleshooting

Error: "password authentication failed for user seguidor"

Causas posibles:

  1. Primera carga del sistema (normal)

    • PostgreSQL todavía está terminando de iniciar
    • Solución: Esperar unos segundos y reintentar
  2. Volumen con contraseña diferente a la actual

    • Solución: Eliminar volumen y redeploy para reinicializar
    docker-compose --project-name lotes down -v
    git commit --allow-empty -m "Reinit DB" && git push
    
  3. Variables de entorno incorrectas

    • Verificar en Gitea > Settings > Actions > Secrets/Variables
    • Variables requeridas: NUXT_POSTGRES_URL, POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB

Error: "relation 'lotes' does not exist"

Causas posibles:

  1. Base de datos no inicializada

    • Verificar logs del workflow: ¿se ejecutó la inicialización?
    • Solución: Click en "🌱 CARGAR DATOS DE EJEMPLO"
  2. Múltiples contenedores PostgreSQL

    • App conectándose al contenedor equivocado
    • Solución: Limpiar contenedores huérfanos
    docker ps -a | grep postgres
    docker rm -f [contenedores antiguos]
    
  3. Usaste TRUNCATE en lugar de DROP

    • Si usaste el botón de borrar BD pero las tablas quedaron vacías
    • Solución: Click en "🌱 CARGAR DATOS DE EJEMPLO" (ahora ejecuta schema primero)

Error: "ENOENT: no such file or directory, open '/app/server/database/02_seed.sql'"

Causa: Archivos SQL no copiados al contenedor de la app

Solución: Verificar que Dockerfile incluya:

COPY --from=builder /app/server/database /app/server/database

Luego rebuild:

git commit -am "Fix Dockerfile" && git push

📊 Logs: Múltiples "Nueva conexión establecida con PostgreSQL"

Comportamiento: Es normal y correcto en desarrollo

  • El pool crea múltiples conexiones (configurado para max 20)
  • Cada endpoint que hace query puede obtener una conexión del pool

Si quieres reducir logs: Ya está configurado para no mostrar en producción

if (process.env.NODE_ENV !== 'production') {
  pool.on('connect', () => console.log('Nueva conexión...'))
}

Queries Útiles

Verificar que todo está correcto

-- 1. Listar tablas
\dt

-- 2. Contar registros
SELECT
  (SELECT COUNT(*) FROM lotes) as total_lotes,
  (SELECT COUNT(*) FROM operaciones) as total_operaciones,
  (SELECT COUNT(*) FROM operacion_lotes) as total_relaciones;

-- 3. Ver lotes creados
SELECT codigo, tipo, cantidad_kg FROM lotes ORDER BY fecha_creado;

-- 4. Probar función de trazabilidad
SELECT * FROM get_trazabilidad(
  (SELECT id FROM lotes WHERE codigo = 'SEC-001')
);

Ver todas las operaciones de un lote

SELECT
  o.tipo AS operacion,
  o.fecha,
  ol.rol,
  ol.cantidad_kg
FROM operacion_lotes ol
JOIN operaciones o ON o.id = ol.operacion_id
WHERE ol.lote_id = (SELECT id FROM lotes WHERE codigo = 'SEC-001')
ORDER BY o.fecha;

Ver inputs directos de un lote

SELECT
  l.codigo,
  l.tipo,
  l.cantidad_kg,
  o.tipo AS operacion_tipo
FROM lotes l
JOIN operacion_lotes ol_in ON ol_in.lote_id = l.id
JOIN operacion_lotes ol_out ON ol_out.operacion_id = ol_in.operacion_id
JOIN operaciones o ON o.id = ol_out.operacion_id
WHERE ol_out.lote_id = (SELECT id FROM lotes WHERE codigo = 'SEC-001')
  AND ol_out.rol = 'output'
  AND ol_in.rol = 'input';

Estadísticas de operaciones

SELECT
  tipo,
  COUNT(*) as total,
  SUM(cantidad_kg) as kg_totales
FROM operaciones
WHERE fecha >= NOW() - INTERVAL '30 days'
GROUP BY tipo
ORDER BY total DESC;

Arquitectura de Inicialización

┌─────────────────────────────────────────────────────────────────┐
│ INICIALIZACIÓN COMPLETA (solo primera vez)                      │
└─────────────────────────────────────────────────────────────────┘

1. docker-compose up
   ↓
2. PostgreSQL container inicia
   ↓
3. Volumen postgres_data vacío? → SÍ
   ↓
4. Ejecuta /docker-entrypoint-initdb.d/ en orden:
   │
   ├─ 01_schema.sql         → Crea tablas, funciones, vistas
   └─ 02_seed.sql           → Inserta 10 lotes, 7 ops, 16 rels
   ↓
5. PostgreSQL termina inicio → healthcheck ✅
   ↓
6. App container inicia
   ↓
7. App intenta conectar (puede fallar 1-2 veces)
   ↓
8. Retry logic espera y reconecta
   ↓
9. ✅ Sistema funcionando


┌─────────────────────────────────────────────────────────────────┐
│ REDESPLIEGUE (datos persisten)                                  │
└─────────────────────────────────────────────────────────────────┘

1. git push → Workflow inicia
   ↓
2. Build nueva imagen Docker
   ↓
3. docker-compose down (sin -v) → PostgreSQL se detiene
   ↓
4. docker-compose up → PostgreSQL reinicia
   ↓
5. Volumen postgres_data existe? → SÍ
   ↓
6. PostgreSQL lee datos existentes
   ↓
7. NO ejecuta scripts de /docker-entrypoint-initdb.d/
   ↓
8. App container inicia con nueva imagen
   ↓
9. ✅ Sistema funcionando (con datos previos)

Backup y Restore

Hacer backup

# Backup completo
docker exec lotes-postgres pg_dump -U seguidor seguidor_lotes > backup_$(date +%Y%m%d).sql

# Backup solo schema (sin datos)
docker exec lotes-postgres pg_dump -U seguidor -s seguidor_lotes > schema_$(date +%Y%m%d).sql

# Backup solo datos
docker exec lotes-postgres pg_dump -U seguidor -a seguidor_lotes > data_$(date +%Y%m%d).sql

Restaurar backup

# Restaurar desde archivo
cat backup_20251121.sql | docker exec -i lotes-postgres psql -U seguidor -d seguidor_lotes

# Restaurar directo desde servidor
docker exec -i lotes-postgres psql -U seguidor -d seguidor_lotes < backup.sql

Resumen de Decisiones Técnicas

Decisión Razón Alternativa Descartada
Cadena de conexión única (NUXT_POSTGRES_URL) Evita discrepancias entre host/puerto/usuario/contraseña Variables sueltas (POSTGRES_*) en el runtime de la app
No tocar pg_hba.conf (usa scram por defecto) Menos acoplamiento a la imagen y a volúmenes existentes Forzar md5 en init scripts
Timeout de 10s en el pool PostgreSQL puede tardar unos segundos en aceptar conexiones Timeout bajo (más falsos negativos)
DROP TABLE en reset Workflow detecta ausencia y reinicializa TRUNCATE (deja tablas vacías)
seed ejecuta schema primero Funciona después de borrar BD Solo seed (falla si no hay tablas)
Scripts SQL en app container Permite endpoint de seed Solo en postgres (no accesible desde app)
Volumen nombrado Persistencia entre redeploys Volumen bind mount (menos portable)

Referencias


Última actualización: 2025-11-21 Autor: Claude Code (claudeCode0@nucleoriofrio.com) Proyecto: Seguidor de Lotes - Sistema de Trazabilidad de Café