Files
analiticaNucleo/nuxt4-app/app/stores
..
2025-09-29 20:57:27 -06:00
2025-09-29 21:18:29 -06:00

📚 Sistema de Stores - Analítica Núcleo

Este directorio contiene el sistema de gestión de estado de la aplicación usando Pinia y localStorage para caché persistente.

📋 Índice


🏗️ Arquitectura General

El sistema de stores se compone de dos partes principales:

app/stores/
├── metadata.ts           # Store de metadatos de tablas
├── tableDataFactory.ts   # Factory para crear stores de datos
└── README.md            # Este archivo

Flujo de Datos

graph TB
    A[App Startup] --> B[Metadata Store]
    B --> C{¿Tiene cache?}
    C -->|Sí| D[Carga desde localStorage]
    C -->|No| E[Fetch desde API]
    E --> F[/api/metadata]
    F --> G[Guarda en localStorage]
    D --> H[Metadata disponible]
    G --> H

    H --> I[Plugin Auto-registro]
    I --> J[Crea stores por tabla]

    K[Usuario selecciona tabla] --> L{¿Store existe?}
    L -->|Sí| M{¿Tiene cache?}
    L -->|No| N[Crea store]
    N --> M
    M -->|Sí| O[Carga desde cache]
    M -->|No| P[Fetch desde API]
    P --> Q[/api/data/:table]
    Q --> R[Guarda en cache]
    O --> S[Datos disponibles]
    R --> S

🗂️ Stores Disponibles

1. Metadata Store (metadata.ts)

Store global que gestiona la información de todas las tablas disponibles.

Responsabilidades:

  • Mantener lista de tablas disponibles
  • Información estructural (columnas, claves primarias, etc.)
  • Estadísticas de cada tabla
  • Cache persistente de metadatos

2. Table Data Stores (Factory pattern)

Stores dinámicos, uno por cada tabla de la base de datos.

Responsabilidades:

  • Almacenar registros de una tabla específica
  • Gestionar estado de carga
  • Cache persistente por tabla
  • Operaciones de filtrado y búsqueda

🔍 Metadata Store

Estructura de Datos

interface TableMetadata {
  table: string              // Nombre de la tabla
  rowCount: number          // Total de registros
  primaryKey: string        // Clave primaria
  approxSizeBytes: number   // Tamaño aproximado en bytes
  columns: string[]         // Lista de columnas
  createdAtRange?: {
    from: string            // Fecha de creación más antigua
    to: string             // Fecha de creación más reciente
  }
  lastRefreshed?: string   // Última actualización de metadatos
  sampleRow?: Record<string, unknown> // Registro de ejemplo
}

Estado del Store

{
  metadata: TableMetadata[]  // Array de metadatos por tabla
  loading: boolean          // Estado de carga
  error: string | null      // Mensaje de error
  lastUpdated: string | null // Timestamp última actualización
  initialized: boolean      // Si el store está inicializado
}

Getters Disponibles

Getter Tipo Descripción
allTables TableMetadata[] Todas las tablas disponibles
getTableMetadata(name) TableMetadata | undefined Metadatos de una tabla específica
totalTables number Cantidad total de tablas
totalRecords number Suma de registros de todas las tablas
tableNames string[] Lista de nombres de tablas
hasMetadata boolean Si hay metadatos cargados
isLoading boolean Si está cargando actualmente
hasError boolean Si hay un error
formattedLastUpdated string Fecha formateada de última actualización
isStale boolean Si los datos tienen > 5 minutos

Actions Disponibles

Action Parámetros Descripción
loadMetadata(force?) force: boolean Carga lazy, solo si necesario
refreshMetadata() - Actualización forzada
clearMetadata() - Limpia cache y datos
initialize() - Inicializa el store (cache + fetch)

Fuente de Datos

Endpoint: GET /api/metadata

Respuesta:

[
  {
    "table": "usuarios",
    "rowCount": 1523,
    "primaryKey": "id",
    "approxSizeBytes": 245760,
    "columns": ["id", "nombre", "email", "created_at"],
    "createdAtRange": {
      "from": "2024-01-01T00:00:00Z",
      "to": "2025-01-20T15:30:00Z"
    }
  },
  // ... más tablas
]

Uso Básico

import { useMetadataStore } from '~/stores/metadata'

// En un componente
const metadataStore = useMetadataStore()

// Inicializar (carga desde cache o API)
await metadataStore.initialize()

// Acceder a los datos
console.log(metadataStore.allTables)
console.log(metadataStore.totalTables)

// Obtener metadatos de una tabla específica
const usuariosMetadata = metadataStore.getTableMetadata('usuarios')

// Refrescar metadatos
await metadataStore.refreshMetadata()

// Verificar frescura de datos
if (metadataStore.isStale) {
  console.log('Los metadatos tienen más de 5 minutos')
}

🏭 Table Data Factory

Sistema factory que crea stores dinámicos para cada tabla.

Estructura de Datos

interface TableDataState<T = Record<string, unknown>> {
  data: T[]                 // Registros cacheados
  loading: boolean          // Estado de carga
  error: string | null      // Mensaje de error
  lastUpdated: string | null // Timestamp última actualización
  initialized: boolean      // Si el store está inicializado
  limit: number            // Límite de registros
}

Getters Disponibles

Getter Tipo Descripción
allRecords T[] Todos los registros cacheados
hasData boolean Si hay datos disponibles
isLoading boolean Si está cargando actualmente
hasError boolean Si hay un error
recordCount number Cantidad de registros
formattedLastUpdated string Fecha formateada
isStale boolean Si los datos tienen > 5 minutos

Actions Disponibles

Action Parámetros Descripción
loadData(force?) force: boolean Carga lazy
refreshData() - Actualización forzada
clearData() - Limpia cache y datos
initialize() - Inicializa (cache + fetch)
getRecord(id) id: string | number Obtiene un registro por ID
filterRecords(predicate) predicate: (record: T) => boolean Filtra registros

Fuente de Datos

Endpoint: GET /api/data/:tableName?limit=100

Respuesta:

{
  "table": "usuarios",
  "count": 100,
  "limit": 100,
  "records": [
    {
      "id": 1,
      "nombre": "Juan Pérez",
      "email": "juan@example.com",
      "created_at": "2024-01-15T10:30:00Z"
    },
    // ... más registros
  ]
}

Creación de Stores

Método 1: Usando la Factory Directamente

import { createTableDataStore } from '~/stores/tableDataFactory'

// Crear un store para la tabla "usuarios"
const useUsuariosStore = createTableDataStore('usuarios', 100)

// Usar el store
const usuariosStore = useUsuariosStore()
await usuariosStore.initialize()

console.log(usuariosStore.allRecords)

Método 2: Usando el Helper

import { useTableDataStore } from '~/stores/tableDataFactory'

// Crear y usar el store en un solo paso
const usuariosStore = useTableDataStore('usuarios', 100)
await usuariosStore.initialize()

Método 3: Usando el Plugin (Recomendado)

// El plugin auto-registra todos los stores
const { $getTableStore } = useNuxtApp()

// Obtener un store ya registrado
const usuariosStore = $getTableStore('usuarios')

if (usuariosStore) {
  await usuariosStore.initialize()
  console.log(usuariosStore.allRecords)
}

🔄 Flujo de Datos

Inicialización de la App

// 1. App.vue o plugin se ejecuta
onMounted(async () => {
  // 2. Metadata store se inicializa
  const metadataStore = useMetadataStore()
  await metadataStore.initialize()

  // 3. Plugin auto-registra stores (tableStores.client.ts)
  // Se crean stores para cada tabla encontrada en metadataStore.allTables

  // 4. Los stores están listos pero NO han cargado datos aún
  // Esto es lazy loading - solo cargan cuando se necesitan
})

Carga de Datos de una Tabla

// Usuario selecciona tabla "usuarios"
const selectTable = async (tableName: string) => {
  // 1. Obtener o crear store
  const store = $getTableStore(tableName) || useTableDataStore(tableName)

  // 2. Inicializar (intenta cache primero)
  await store.initialize()

  // Flujo interno de initialize():
  // ├─ loadFromCache()
  // │  └─ Lee localStorage['table-data-usuarios']
  // │     ├─ Si existe → usa datos cacheados
  // │     └─ Si no existe → continúa
  // └─ loadData()
  //    └─ fetchData()
  //       └─ $fetch('/api/data/usuarios?limit=100')
  //          └─ Guarda en localStorage
  //          └─ Actualiza store

  // 3. Datos disponibles
  console.log(store.allRecords)
}

Actualización Manual

// Usuario hace click en "Actualizar datos"
const refreshTable = async () => {
  // 1. Ejecutar refreshData()
  await store.refreshData()

  // Flujo interno de refreshData():
  // └─ fetchData()
  //    ├─ loading = true
  //    ├─ $fetch('/api/data/usuarios?limit=100')
  //    ├─ Actualiza store.data
  //    ├─ Actualiza lastUpdated
  //    ├─ Guarda en localStorage
  //    └─ loading = false
}

💻 Uso en Componentes

Ejemplo Completo: Explorador de Tablas

<template>
  <div>
    <!-- Selector de tabla -->
    <select v-model="selectedTable" @change="onTableChange">
      <option v-for="table in metadataStore.tableNames" :key="table">
        {{ table }}
      </option>
    </select>

    <!-- Información de cache -->
    <div v-if="currentStore">
      <p>Última actualización: {{ currentStore.formattedLastUpdated }}</p>
      <p v-if="currentStore.isStale" class="warning">
         Datos desactualizados
      </p>
      <button @click="refreshData" :disabled="currentStore.isLoading">
        {{ currentStore.isLoading ? 'Actualizando...' : 'Actualizar' }}
      </button>
    </div>

    <!-- Tabla de datos -->
    <div v-if="currentStore?.hasData">
      <p>{{ currentStore.recordCount }} registros</p>
      <table>
        <thead>
          <tr>
            <th v-for="col in columns" :key="col">{{ col }}</th>
          </tr>
        </thead>
        <tbody>
          <tr v-for="record in currentStore.allRecords" :key="record.id">
            <td v-for="col in columns" :key="col">
              {{ record[col] }}
            </td>
          </tr>
        </tbody>
      </table>
    </div>

    <!-- Estados -->
    <div v-else-if="currentStore?.isLoading">Cargando...</div>
    <div v-else-if="currentStore?.hasError">
      Error: {{ currentStore.error }}
    </div>
    <div v-else>Selecciona una tabla</div>
  </div>
</template>

<script setup lang="ts">
import { useMetadataStore } from '~/stores/metadata'
import { useTableDataStore } from '~/stores/tableDataFactory'

const metadataStore = useMetadataStore()
const selectedTable = ref('')
const currentStore = ref<ReturnType<typeof useTableDataStore> | null>(null)

// Columnas de la tabla actual
const columns = computed(() => {
  if (!currentStore.value?.hasData) return []
  const firstRecord = currentStore.value.allRecords[0]
  return Object.keys(firstRecord)
})

// Cambio de tabla
async function onTableChange() {
  if (!selectedTable.value) return

  // Obtener store de la tabla
  currentStore.value = useTableDataStore(selectedTable.value)

  // Inicializar (usa cache si existe)
  await currentStore.value.initialize()
}

// Refrescar datos
async function refreshData() {
  if (!currentStore.value) return
  await currentStore.value.refreshData()
}

// Inicializar metadatos al montar
onMounted(async () => {
  await metadataStore.initialize()

  // Auto-seleccionar primera tabla
  if (metadataStore.tableNames.length > 0) {
    selectedTable.value = metadataStore.tableNames[0]
    await onTableChange()
  }
})
</script>

Ejemplo: Búsqueda y Filtrado

import { useTableDataStore } from '~/stores/tableDataFactory'

const usuariosStore = useTableDataStore('usuarios')
await usuariosStore.initialize()

// Filtrar usuarios activos
const usuariosActivos = usuariosStore.filterRecords(
  user => user.activo === true
)

// Buscar usuario por ID
const usuario = usuariosStore.getRecord(123)

// Búsqueda personalizada
const usuariosPorEmail = usuariosStore.filterRecords(
  user => user.email.includes('@gmail.com')
)

console.log(`Encontrados ${usuariosPorEmail.length} usuarios con Gmail`)

📖 API Reference

Metadata Store API

interface MetadataStore {
  // State
  metadata: TableMetadata[]
  loading: boolean
  error: string | null
  lastUpdated: string | null
  initialized: boolean

  // Getters
  allTables: TableMetadata[]
  getTableMetadata(name: string): TableMetadata | undefined
  totalTables: number
  totalRecords: number
  tableNames: string[]
  hasMetadata: boolean
  isLoading: boolean
  hasError: boolean
  formattedLastUpdated: string
  isStale: boolean

  // Actions
  loadMetadata(force?: boolean): Promise<void>
  refreshMetadata(): Promise<void>
  clearMetadata(): void
  initialize(): Promise<void>
}

Table Data Store API

interface TableDataStore<T> {
  // State
  data: T[]
  loading: boolean
  error: string | null
  lastUpdated: string | null
  initialized: boolean
  limit: number

  // Getters
  allRecords: T[]
  hasData: boolean
  isLoading: boolean
  hasError: boolean
  recordCount: number
  formattedLastUpdated: string
  isStale: boolean

  // Actions
  loadData(force?: boolean): Promise<void>
  refreshData(): Promise<void>
  clearData(): void
  initialize(): Promise<void>
  getRecord(id: string | number): T | undefined
  filterRecords(predicate: (record: T) => boolean): T[]
}

💾 Cache y Persistencia

Estrategia de Cache

El sistema usa localStorage para persistir datos:

// Estructura de cache en localStorage
{
  // Metadatos
  "metadata-cache": {
    metadata: TableMetadata[],
    lastUpdated: "2025-01-20T10:30:00Z"
  },

  // Datos de tabla usuarios
  "table-data-usuarios": {
    data: [...registros],
    lastUpdated: "2025-01-20T10:35:00Z",
    limit: 100
  },

  // Datos de tabla productos
  "table-data-productos": {
    data: [...registros],
    lastUpdated: "2025-01-20T10:40:00Z",
    limit: 100
  }
}

Políticas de Cache

  1. Freshness Check: Los datos se consideran "stale" después de 5 minutos
  2. Lazy Loading: Los stores solo cargan cuando se accede a ellos
  3. Cache-First: Siempre intenta cargar desde cache primero
  4. Auto-Refresh: El usuario controla manualmente cuándo refrescar
  5. Offline Support: Datos disponibles incluso sin conexión

Limpieza de Cache

// Limpiar cache de metadatos
const metadataStore = useMetadataStore()
metadataStore.clearMetadata()

// Limpiar cache de una tabla
const usuariosStore = useTableDataStore('usuarios')
usuariosStore.clearData()

// Limpiar TODO el cache (localStorage)
if (process.client) {
  localStorage.clear()
}

🎯 Ejemplos Avanzados

Sincronización Automática

// Auto-refrescar cada 5 minutos si los datos están stale
const autoRefresh = () => {
  const store = useTableDataStore('usuarios')

  setInterval(async () => {
    if (store.isStale && !store.isLoading) {
      console.log('Refrescando datos automáticamente...')
      await store.refreshData()
    }
  }, 5 * 60 * 1000) // 5 minutos
}

Validación de Datos

// Verificar integridad de datos cacheados
const validateCache = async (tableName: string) => {
  const store = useTableDataStore(tableName)
  const metadata = metadataStore.getTableMetadata(tableName)

  if (!metadata) return false

  // Verificar que el número de registros sea razonable
  if (store.recordCount > metadata.rowCount) {
    console.warn('Cache corrupto, limpiando...')
    store.clearData()
    await store.refreshData()
    return false
  }

  return true
}

Composable Personalizado

// composables/useTableData.ts
export function useTableData(tableName: string) {
  const store = useTableDataStore(tableName)
  const initialized = ref(false)

  onMounted(async () => {
    await store.initialize()
    initialized.value = true
  })

  // Auto-refrescar si stale
  watch(() => store.isStale, async (isStale) => {
    if (isStale && initialized.value) {
      await store.refreshData()
    }
  })

  return {
    data: computed(() => store.allRecords),
    loading: computed(() => store.isLoading),
    error: computed(() => store.error),
    refresh: () => store.refreshData()
  }
}

TypeScript Tipado

// Definir tipos para tus tablas
interface Usuario {
  id: number
  nombre: string
  email: string
  activo: boolean
  created_at: string
}

interface Producto {
  id: number
  nombre: string
  precio: number
  stock: number
}

// Usar stores tipados
const usuariosStore = useTableDataStore<Usuario>('usuarios')
const productosStore = useTableDataStore<Producto>('productos')

// TypeScript ahora conoce la estructura
usuariosStore.allRecords.forEach(user => {
  console.log(user.email) // ✅ TypeScript sabe que existe
  // console.log(user.foo) // ❌ Error: Property 'foo' does not exist
})

🐛 Debugging

Inspeccionar Estado

// En DevTools Console
import { useMetadataStore } from '~/stores/metadata'
import { useTableDataStore } from '~/stores/tableDataFactory'

const metadataStore = useMetadataStore()
const usuariosStore = useTableDataStore('usuarios')

// Ver estado completo
console.log('Metadata:', metadataStore.$state)
console.log('Usuarios:', usuariosStore.$state)

// Ver cache
console.log('Cache:', {
  metadata: localStorage.getItem('metadata-cache'),
  usuarios: localStorage.getItem('table-data-usuarios')
})

Logs Detallados

Descomenta los console.log en los stores para ver el flujo completo:

// En tableDataFactory.ts línea 144
console.log(`[${tableName}] Fetching data...`)

// En tableDataFactory.ts línea 158
console.log(`[${tableName}] Data fetched: ${this.recordCount} records`)

// En tableDataFactory.ts línea 164
console.log(`[${tableName}] Saved to cache`)

📝 Notas Importantes

⚠️ Limitaciones

  1. Límite de localStorage: ~5-10MB por dominio

    • Solución: Implementar estrategia LRU si se excede
  2. Límite de registros: Por defecto 100 registros por tabla

    • Modificable en la factory: useTableDataStore('tabla', 500)
  3. Sin paginación: Todos los registros se cargan de una vez

    • Para tablas grandes, considerar implementar paginación
  4. Sin sincronización real-time: Los datos se actualizan manualmente

    • Para real-time, considerar WebSockets

🚀 Performance Tips

  1. Lazy Loading: Los stores no cargan datos hasta que se necesitan
  2. Cache-First: Usa cache para respuesta instantánea
  3. Selectores Computados: Usa computed() para filtros complejos
  4. Debounce: Para búsquedas, usa debounce para evitar renders excesivos

🔒 Seguridad

  • Los datos en localStorage NO están encriptados
  • No almacenes información sensible (contraseñas, tokens)
  • El API backend debe validar permisos y autenticación

📚 Referencias


Última actualización: 2025-01-20 Versión: 1.0.0 Autor: Equipo Núcleo