Fecha: 2025-01-22 Prioridad: 🔴 CRÍTICA Estado: En análisis
Problema reportado: "Error al abrir conversaciones del historial"
Comportamiento observado:
- Usuario puede guardar conversaciones nuevas
- Al intentar cargar conversación desde el sidebar, ocurre un error
- Posible error de hidratación o tipo en el frontend
Respuesta: Los mensajes cargados desde la base de datos tienen timestamps como strings, pero el componente espera Date objects.
Evidencia:
conversation-store.ts:96hace cast directo:return data as unknown as Conversation- No hay transformación de datos entre DB y frontend
- Supabase guarda timestamps como ISO strings en JSONB
Respuesta: Supabase almacena todo el campo messages como JSONB, lo cual serializa los Dates a strings ISO.
Evidencia:
// conversation-store.ts:41
messages: messages as unknown as JsonValue,Cuando se guarda:
// Timestamp original (frontend)
timestamp: new Date() // Date object
// Supabase lo convierte a:
timestamp: "2025-01-22T20:00:00.000Z" // stringRespuesta: La función loadConversation() no tiene lógica de transformación de datos.
Evidencia:
// conversation-store.ts:82-96
export async function loadConversation(conversationId: string) {
const { data, error } = await supabase
.from('conversations')
.select('*')
.eq('id', conversationId)
.single()
// ❌ NO HAY TRANSFORMACIÓN - Cast directo
return data as unknown as Conversation
}Comparación con guardar:
- Al guardar: ✅ Usa type casting explícito para JSONB
- Al cargar: ❌ No hay parsing ni validación
Respuesta: React / TypeScript esperan Date pero reciben string, causando:
- Type Mismatch:
// chat/page.tsx:48
setMessages(conversation.messages)
// messages tiene timestamp: string (de DB)
// pero Message interface define timestamp: Date- Errores en rendering:
// ChatMessage puede intentar:
message.timestamp.toLocaleString() // ❌ Error: string no tiene este método- Hidration errors:
- Server renderiza con strings
- Client espera Date objects
- React detecta mismatch
🎯 ROOT CAUSE: Falta de capa de serialización/deserialización entre DB y aplicación
Problemas arquitecturales:
-
No hay Data Transfer Objects (DTOs)
- Frontend y DB comparten mismas interfaces
- No hay separación de concerns
- Cambios en DB afectan directamente frontend
-
No hay validación de esquema
- Cast con
as unknown as Tbypasea type checking - No se valida estructura de datos de DB
- TypeScript no puede ayudar en runtime
- Cast con
-
No hay transformación de tipos
- Dates ↔ ISO strings no se manejan
- JSONB ↔ TypeScript objects sin validación
- Metadata puede tener estructura inconsistente
-
Inconsistencia en el ciclo de vida:
┌──────────────────────────────────────────────────────┐
│ FRONTEND (TypeScript types) │
│ ↓ Save: Date → string (implícito por Supabase) │
│ DATABASE (JSONB: todos strings) │
│ ↓ Load: string → NO TRANSFORMATION ❌ │
│ FRONTEND (espera Date pero recibe string) 💥 │
└──────────────────────────────────────────────────────┘
// 1. Frontend crea mensaje
const message = {
id: "123",
role: "user",
content: "Hola",
timestamp: new Date() // ✅ Date object
}
// 2. Se guarda en DB
await saveConversation(userId, [message])
// Supabase convierte: new Date() → "2025-01-22T20:00:00.000Z"
// 3. Se carga de DB
const conversation = await loadConversation(conversationId)
// conversation.messages[0].timestamp = "2025-01-22T20:00:00.000Z" ❌ string
// 4. Frontend intenta usar
setMessages(conversation.messages)
// React recibe timestamp: string cuando espera Date
// 💥 ERROR: Type mismatch, hydration error, o runtime error// conversation-store.ts
// DTO para la base de datos
interface ConversationDB {
id: string
user_id: string
messages: Array<{
id: string
role: 'user' | 'assistant'
content: string
timestamp: string // ← ISO string en DB
}>
metadata: {
title?: string
created_at: string
updated_at: string
}
}
// Interface para el frontend (ya existe)
export interface Conversation {
id: string
user_id: string
messages: Message[] // timestamp es Date
metadata: {
title?: string
created_at: string
updated_at: string
}
}// Helpers de transformación
function dbMessageToMessage(dbMessage: any): Message {
return {
id: dbMessage.id,
role: dbMessage.role,
content: dbMessage.content,
timestamp: new Date(dbMessage.timestamp) // ✅ string → Date
}
}
function messageTodb Message(message: Message): any {
return {
id: message.id,
role: message.role,
content: message.content,
timestamp: message.timestamp.toISOString() // ✅ Date → string
}
}export async function loadConversation(conversationId: string): Promise<Conversation | null> {
const supabase = await createClient()
const { data, error } = await supabase
.from('conversations')
.select('*')
.eq('id', conversationId)
.single()
if (error) {
console.error('Error loading conversation:', error)
return null
}
if (!data) return null
// ✅ TRANSFORMACIÓN EXPLÍCITA
const conversation: Conversation = {
id: data.id,
user_id: data.user_id,
messages: (data.messages as any[]).map(dbMessageToMessage), // ✅ Transformar cada mensaje
metadata: data.metadata as any
}
return conversation
}import { z } from 'zod'
const MessageSchema = z.object({
id: z.string(),
role: z.enum(['user', 'assistant']),
content: z.string(),
timestamp: z.string().transform(str => new Date(str)) // ✅ Valida Y transforma
})
const ConversationSchema = z.object({
id: z.string(),
user_id: z.string(),
messages: z.array(MessageSchema),
metadata: z.object({
title: z.string().optional(),
created_at: z.string(),
updated_at: z.string()
})
})
// En loadConversation:
const validated = ConversationSchema.parse(data)
return validated- ✅ Modificar
loadConversation()para transformar timestamps - ✅ Modificar
getUserConversations()igual - ✅ Agregar try-catch para manejar errores de parsing
- ✅ Logging para debugging
- ✅ Crear
types/conversation-dto.tscon interfaces DB y Frontend - ✅ Crear
lib/chat/transformers.tscon funciones de transformación - ✅ Instalar y configurar Zod para validación
- ✅ Actualizar todos los conversation stores
- ✅ Tests unitarios para transformers
- ✅ Documentar convenciones de serialización
- ✅ ESLint rule para evitar
as unknown as T - ✅ Code review checklist para DB operations
- ✅ Agregar integration tests
Ventajas:
- ✅ Fix rápido sin refactor grande
- ✅ No rompe código existente
- ✅ Fácil de validar
- ✅ Se puede hacer Fase 2 después
Desventajas:
⚠️ No previene futuros problemas similares⚠️ Deuda técnica permanece
Trade-off aceptable: Arreglar el bug ahora, refactor después.
/**
* Transform DB message (timestamp as string) to frontend Message (timestamp as Date)
*/
function transformMessage(dbMessage: any): Message {
return {
id: dbMessage.id,
role: dbMessage.role,
content: dbMessage.content,
timestamp: new Date(dbMessage.timestamp),
}
}export async function loadConversation(conversationId: string): Promise<Conversation | null> {
const supabase = await createClient()
const { data, error } = await supabase
.from('conversations')
.select('*')
.eq('id', conversationId)
.single()
if (error) {
console.error('Error loading conversation:', error)
return null
}
if (!data) return null
// Transform messages: string timestamps → Date objects
try {
const transformedMessages = (data.messages as any[]).map(transformMessage)
return {
id: data.id,
user_id: data.user_id,
messages: transformedMessages,
metadata: data.metadata as any,
}
} catch (err) {
console.error('Error transforming conversation data:', err)
return null
}
}export async function getUserConversations(userId: string): Promise<Conversation[]> {
const supabase = await createClient()
const { data, error } = await supabase
.from('conversations')
.select('*')
.eq('user_id', userId)
.order('metadata->updated_at', { ascending: false })
if (error) {
console.error('Error loading conversations:', error)
return []
}
if (!data) return []
// Transform each conversation
try {
return data.map(conv => ({
id: conv.id,
user_id: conv.user_id,
messages: (conv.messages as any[]).map(transformMessage),
metadata: conv.metadata as any,
}))
} catch (err) {
console.error('Error transforming conversations data:', err)
return []
}
}-
✅ Fix del bug principal:
- Conversaciones cargan correctamente
- Timestamps son Date objects válidos
- No más type mismatches
-
✅ Error handling robusto:
- Try-catch previene crashes
- Logs para debugging
- Retorna null/[] en caso de error
-
✅ Type safety mejorada:
- Transformación explícita documentada
- Menos reliance en
as unknown as T - TypeScript puede rastrear mejor los tipos
-
✅ Mantiene compatibilidad:
- No breaking changes
- Misma API pública
- Funciona con código existente
-
Crear conversación:
- Chat nuevo
- Enviar mensaje
- Verificar que se guarda
-
Cargar conversación:
- Refresh página
- Abrir sidebar
- Click en conversación vieja
- Verificar: No hay error
- Verificar: Mensajes aparecen correctamente
- Verificar: Timestamps se muestran bien
-
Múltiples conversaciones:
- Crear 3-4 conversaciones
- Alternar entre ellas
- Verificar que cada una carga correctamente
-
Edge cases:
- Conversación con 1 solo mensaje
- Conversación muy antigua
- Conversación con muchos mensajes (50+)
✅ No debe haber: "Error loading conversation"
✅ No debe haber: "Error transforming"
✅ No debe haber: Hydration errors
✅ No debe haber: Type errors en runtime
| Métrica | Antes | Después (Esperado) |
|---|---|---|
| Conversaciones cargan | ❌ Error | ✅ 100% success |
| Type errors | ✅ 0 errors | |
| Console errors | 🔴 Múltiples | ✅ 0 errors |
| User experience | 💔 Roto | ✅ Funcional |
- Implementar DTOs separados
- Agregar Zod validation
- Crear transformers module
- Documentar serialization patterns
- Crear guía de contribution
- ESLint rules custom
- Unit tests para transformers
- Integration tests para conversation flow
- E2E tests con Playwright
Estado: Listo para implementación Prioridad: 🔴 CRÍTICA Esfuerzo estimado: 15 minutos Risk: Bajo (cambios aislados con fallbacks)