📚 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
- Stores Disponibles
- Metadata Store
- Table Data Factory
- Flujo de Datos
- Uso en Componentes
- API Reference
- Cache y Persistencia
- Ejemplos Avanzados
🏗️ 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
- Freshness Check: Los datos se consideran "stale" después de 5 minutos
- Lazy Loading: Los stores solo cargan cuando se accede a ellos
- Cache-First: Siempre intenta cargar desde cache primero
- Auto-Refresh: El usuario controla manualmente cuándo refrescar
- 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
-
Límite de localStorage: ~5-10MB por dominio
- Solución: Implementar estrategia LRU si se excede
-
Límite de registros: Por defecto 100 registros por tabla
- Modificable en la factory:
useTableDataStore('tabla', 500)
- Modificable en la factory:
-
Sin paginación: Todos los registros se cargan de una vez
- Para tablas grandes, considerar implementar paginación
-
Sin sincronización real-time: Los datos se actualizan manualmente
- Para real-time, considerar WebSockets
🚀 Performance Tips
- Lazy Loading: Los stores no cargan datos hasta que se necesitan
- Cache-First: Usa cache para respuesta instantánea
- Selectores Computados: Usa
computed()para filtros complejos - 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