Eliminados hacks de autenticación md5 y configuración manual de pg_hba.conf. Ahora usa NUXT_POSTGRES_URL como secret de Gitea para conexión directa.
16 KiB
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
- ⚠️ Peculiaridades de Implementación
- Ejecución de Scripts
- Troubleshooting
- Queries Útiles
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 desderuntimeConfig.postgresUrl. - Formato recomendado:
postgres://seguidor:seguidor_password@postgres:5432/seguidor_lotes. - El driver
pgsoportascram-sha-256(default de Postgres 16), no se tocapg_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):
01_schema.sql- Crea estructura (tablas, funciones, vistas)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,
TRUNCATEfalla - ✅ Por eso el endpoint
seed-databaseejecuta 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 downydocker-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)
- Click en "🗑️ BORRAR TODA LA BD" → Elimina todas las tablas
- Click en "🌱 CARGAR DATOS DE EJEMPLO" → Recrea schema + seed
- 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:
- Cadena única: evita discrepancias entre host/puerto/usuario/contraseña.
- 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(noTRUNCATE) 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:
-
Primera carga del sistema (normal)
- PostgreSQL todavía está terminando de iniciar
- Solución: Esperar unos segundos y reintentar
-
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 -
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:
-
Base de datos no inicializada
- Verificar logs del workflow: ¿se ejecutó la inicialización?
- Solución: Click en "🌱 CARGAR DATOS DE EJEMPLO"
-
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] -
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
- PostgreSQL Authentication: https://www.postgresql.org/docs/16/auth-pg-hba-conf.html
- node-postgres (pg): https://node-postgres.com/
- Docker Init Scripts: https://hub.docker.com/_/postgres (sección "Initialization scripts")
- Pool de Conexiones: https://node-postgres.com/apis/pool
- Recursive Queries (CTE): https://www.postgresql.org/docs/current/queries-with.html
Última actualización: 2025-11-21 Autor: Claude Code (claudeCode0@nucleoriofrio.com) Proyecto: Seguidor de Lotes - Sistema de Trazabilidad de Café