Files
analiticaNucleo/nuxt4-app/app/stores/README.md

794 lines
20 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 📚 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](#-arquitectura-general)
- [Stores Disponibles](#-stores-disponibles)
- [Metadata Store](#-metadata-store)
- [Table Data Factory](#-table-data-factory)
- [Flujo de Datos](#-flujo-de-datos)
- [Uso en Componentes](#-uso-en-componentes)
- [API Reference](#-api-reference)
- [Cache y Persistencia](#-cache-y-persistencia)
- [Ejemplos Avanzados](#-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
```mermaid
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
```typescript
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
```typescript
{
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:**
```json
[
{
"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
```typescript
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
```typescript
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:**
```json
{
"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
```typescript
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
```typescript
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)
```typescript
// 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
```typescript
// 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
```typescript
// 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
```typescript
// 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
```vue
<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
```typescript
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
```typescript
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
```typescript
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:
```typescript
// 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
```typescript
// 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
```typescript
// 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
```typescript
// 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
```typescript
// 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
```typescript
// 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
```typescript
// 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:
```typescript
// 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
- [Pinia Documentation](https://pinia.vuejs.org/)
- [Nuxt 4 Documentation](https://nuxt.com/)
- [LocalStorage API](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage)
---
**Última actualización:** 2025-01-20
**Versión:** 1.0.0
**Autor:** Equipo Núcleo