diff --git a/nuxt4-app/app/components/MetadatosCard.vue b/nuxt4-app/app/components/MetadatosCard.vue index 2faea79..ff8c5a3 100644 --- a/nuxt4-app/app/components/MetadatosCard.vue +++ b/nuxt4-app/app/components/MetadatosCard.vue @@ -124,6 +124,7 @@ const isLoadingAll = ref(false) const loadingProgress = ref(0) // Get the table store for this specific datasource (using name, not table) +// The plugin has already loaded all caches, so this just retrieves the existing instance const tableStore = computed(() => { if (typeof $getTableStore === 'function') { return $getTableStore(props.metadata.name) diff --git a/nuxt4-app/app/pages/explorer.vue b/nuxt4-app/app/pages/explorer.vue index 5fb1823..f47b85a 100644 --- a/nuxt4-app/app/pages/explorer.vue +++ b/nuxt4-app/app/pages/explorer.vue @@ -277,8 +277,12 @@ const totalRowCount = computed(() => { }) // Methods -function selectTable(tableName: string) { - if (selectedTableName.value === tableName) return +async function selectTable(tableName: string) { + console.log(`[Explorer] selectTable called with: ${tableName}`) + if (selectedTableName.value === tableName) { + console.log(`[Explorer] Table ${tableName} already selected, skipping`) + return + } selectedTableName.value = tableName @@ -286,20 +290,31 @@ function selectTable(tableName: string) { if (typeof $getTableStore === 'function') { const store = $getTableStore(tableName) if (store) { + console.log(`[Explorer] Got store for ${tableName} via plugin`) currentTableStore.value = store - // Initialize the store (loads from cache or fetches) - store.initialize() + + // Load from cache first (async from IndexedDB) + await store.loadFromCache() + console.log(`[Explorer] After loadFromCache, store has ${store.recordCount} records`) + + // Note: We don't call initialize() here because MetadatosCard will handle loading + // initialize() would trigger a fetch, but we want manual control via the buttons } } else { // Fallback: create store directly + console.log(`[Explorer] Creating store for ${tableName} directly (fallback)`) currentTableStore.value = useTableDataStore(tableName) - currentTableStore.value.initialize() + await currentTableStore.value.loadFromCache() + console.log(`[Explorer] After loadFromCache, store has ${currentTableStore.value.recordCount} records`) } } async function refreshTableData() { + console.log('[Explorer] refreshTableData called') if (currentTableStore.value) { + console.log(`[Explorer] Refreshing data for current table (${selectedTableName.value})`) await currentTableStore.value.refreshData() + console.log(`[Explorer] After refresh, store has ${currentTableStore.value.recordCount} records`) } } @@ -340,13 +355,15 @@ function formatCellValue(value: unknown): string { // Lifecycle onMounted(async () => { + console.log('[Explorer] onMounted: Initializing metadata store') await metadataStore.initialize() // Auto-select first table if available if (metadataStore.hasMetadata && !selectedTableName.value) { const firstTable = metadataStore.allTables[0] if (firstTable) { - selectTable(firstTable.table) + console.log(`[Explorer] Auto-selecting first table: ${firstTable.name}`) + selectTable(firstTable.name) // Use name instead of table } } }) @@ -354,7 +371,8 @@ onMounted(async () => { // Auto-select first table when metadata becomes available watch(() => metadataStore.hasMetadata, (hasMetadata) => { if (hasMetadata && !selectedTableName.value && metadataStore.allTables.length > 0) { - selectTable(metadataStore.allTables[0].table) + console.log('[Explorer] Metadata became available, auto-selecting first table') + selectTable(metadataStore.allTables[0].name) // Use name instead of table } }) \ No newline at end of file diff --git a/nuxt4-app/plugins/loading.client.ts b/nuxt4-app/app/plugins/loading.client.ts similarity index 100% rename from nuxt4-app/plugins/loading.client.ts rename to nuxt4-app/app/plugins/loading.client.ts diff --git a/nuxt4-app/app/plugins/tableStores.client.ts b/nuxt4-app/app/plugins/tableStores.client.ts new file mode 100644 index 0000000..7b50107 --- /dev/null +++ b/nuxt4-app/app/plugins/tableStores.client.ts @@ -0,0 +1,64 @@ +import { useMetadataStore } from '~/stores/metadata' +import { createTableDataStore } from '~/stores/tableDataFactory' + +export default defineNuxtPlugin(async (nuxtApp) => { + console.log('[TableStoresPlugin] Initializing...') + + // Wait for metadata to be available + const metadataStore = useMetadataStore() + + // Initialize metadata first + await metadataStore.initialize() + console.log(`[TableStoresPlugin] Metadata initialized, found ${metadataStore.allTables.length} tables`) + + // Create stores for all available tables and preload from cache + const tableStores = new Map>() + const storeInstances = new Map>>() + + // Load all caches in parallel for better performance + const cacheLoadPromises = metadataStore.allTables.map(async (metadata) => { + const datasourceName = metadata.name // Use datasource name, not table name + console.log(`[TableStoresPlugin] Creating store for datasource: ${datasourceName}`) + + const storeFactory = createTableDataStore(datasourceName, 100) + const storeInstance = storeFactory() + + // Register both factory and instance + tableStores.set(datasourceName, storeFactory) + storeInstances.set(datasourceName, storeInstance) + + // Load from cache immediately + await storeInstance.loadFromCache() + console.log(`[TableStoresPlugin] Loaded ${storeInstance.recordCount} records for ${datasourceName}`) + }) + + // Wait for all caches to load + await Promise.all(cacheLoadPromises) + console.log('[TableStoresPlugin] All caches loaded successfully') + + // Provide access to table stores + return { + provide: { + tableStores, + // Helper function to get a table store (returns existing instance) + getTableStore: (tableName: string) => { + // First try to get existing instance + const existingInstance = storeInstances.get(tableName) + if (existingInstance) { + return existingInstance + } + + // Fall back to creating new instance from factory + const storeFactory = tableStores.get(tableName) + if (!storeFactory) { + console.warn(`[TableStoresPlugin] Table store for "${tableName}" not found`) + return null + } + + const newInstance = storeFactory() + storeInstances.set(tableName, newInstance) + return newInstance + } + } + } +}) \ No newline at end of file diff --git a/nuxt4-app/app/stores/tableDataFactory.ts b/nuxt4-app/app/stores/tableDataFactory.ts index 76889ac..0e43160 100644 --- a/nuxt4-app/app/stores/tableDataFactory.ts +++ b/nuxt4-app/app/stores/tableDataFactory.ts @@ -1,4 +1,5 @@ import { defineStore } from 'pinia' +import { tableDataStorage } from '~/utils/storage' export interface TableDataState> { data: T[] @@ -23,8 +24,8 @@ export interface TableDataActions> { loadData(force?: boolean): Promise refreshData(): Promise fetchData(): Promise - clearData(): void - loadFromCache(): void + clearData(): Promise + loadFromCache(): Promise extractErrorMessage(error: unknown): string initialize(): Promise getRecord(id: string | number): T | undefined @@ -169,16 +170,16 @@ export function createTableDataStore>( this.lastUpdated = new Date().toISOString() this.initialized = true - // Persist to localStorage for offline access + // Persist to IndexedDB for offline access if (process.client) { try { - localStorage.setItem(cacheKey, JSON.stringify({ + await tableDataStorage.setItem(cacheKey, { data: this.data, lastUpdated: this.lastUpdated, limit: this.limit - })) + }) } catch (error) { - console.warn(`Failed to persist ${tableName} data to localStorage:`, error) + console.warn(`Failed to persist ${tableName} data to IndexedDB:`, error) } } } catch (error: any) { @@ -195,9 +196,9 @@ export function createTableDataStore>( }, /** - * Load data from localStorage cache + * Load data from IndexedDB cache */ - loadFromCache(): void { + async loadFromCache(): Promise { if (!process.client) { console.log(`[${tableName}] loadFromCache: Not on client, skipping`) return @@ -205,9 +206,8 @@ export function createTableDataStore>( try { console.log(`[${tableName}] loadFromCache: Attempting to load from key: ${cacheKey}`) - const cached = localStorage.getItem(cacheKey) - if (cached) { - const parsedCache = JSON.parse(cached) + const parsedCache = await tableDataStorage.getItem(cacheKey) + if (parsedCache) { console.log(`[${tableName}] loadFromCache: Found ${parsedCache.data?.length || 0} records in cache`) this.data = parsedCache.data || [] this.lastUpdated = parsedCache.lastUpdated || null @@ -225,7 +225,7 @@ export function createTableDataStore>( /** * Clear all data */ - clearData(): void { + async clearData(): Promise { this.data = [] this.error = null this.lastUpdated = null @@ -233,7 +233,7 @@ export function createTableDataStore>( if (process.client) { try { - localStorage.removeItem(cacheKey) + await tableDataStorage.removeItem(cacheKey) } catch (error) { console.warn(`Failed to clear ${tableName} data cache:`, error) } @@ -260,7 +260,7 @@ export function createTableDataStore>( */ async initialize(): Promise { // Load from cache first for immediate availability - this.loadFromCache() + await this.loadFromCache() // Then try to fetch fresh data await this.loadData() @@ -349,30 +349,29 @@ export function createTableDataStore>( console.log(`[${tableName}] loadAllDataInBatches: Finished loading ${this.data.length} records`) - // Persist to localStorage + // Persist to IndexedDB if (process.client) { try { - console.log(`[${tableName}] loadAllDataInBatches: Persisting to localStorage with key: ${cacheKey}`) + console.log(`[${tableName}] loadAllDataInBatches: Persisting to IndexedDB with key: ${cacheKey}`) const dataToSave = { data: this.data, lastUpdated: this.lastUpdated, limit: this.limit } console.log(`[${tableName}] loadAllDataInBatches: Saving ${dataToSave.data.length} records`) - localStorage.setItem(cacheKey, JSON.stringify(dataToSave)) - console.log(`[${tableName}] loadAllDataInBatches: Successfully saved to localStorage`) + await tableDataStorage.setItem(cacheKey, dataToSave) + console.log(`[${tableName}] loadAllDataInBatches: Successfully saved to IndexedDB`) // Verify it was saved - const saved = localStorage.getItem(cacheKey) + const saved = await tableDataStorage.getItem(cacheKey) if (saved) { - const parsed = JSON.parse(saved) - console.log(`[${tableName}] loadAllDataInBatches: Verification - localStorage contains ${parsed.data?.length || 0} records`) + console.log(`[${tableName}] loadAllDataInBatches: Verification - IndexedDB contains ${saved.data?.length || 0} records`) } } catch (error) { - console.error(`[${tableName}] loadAllDataInBatches: Failed to persist data to localStorage:`, error) + console.error(`[${tableName}] loadAllDataInBatches: Failed to persist data to IndexedDB:`, error) } } else { - console.log(`[${tableName}] loadAllDataInBatches: Not on client, skipping localStorage`) + console.log(`[${tableName}] loadAllDataInBatches: Not on client, skipping IndexedDB`) } if (onProgress) { @@ -466,30 +465,29 @@ export function createTableDataStore>( this.lastUpdated = new Date().toISOString() console.log(`[${tableName}] loadLatestDataInBatches: Added ${newRecordsCount} new records, total: ${this.data.length}`) - // Persist to localStorage + // Persist to IndexedDB if (process.client) { try { - console.log(`[${tableName}] loadLatestDataInBatches: Persisting to localStorage with key: ${cacheKey}`) + console.log(`[${tableName}] loadLatestDataInBatches: Persisting to IndexedDB with key: ${cacheKey}`) const dataToSave = { data: this.data, lastUpdated: this.lastUpdated, limit: this.limit } console.log(`[${tableName}] loadLatestDataInBatches: Saving ${dataToSave.data.length} records`) - localStorage.setItem(cacheKey, JSON.stringify(dataToSave)) - console.log(`[${tableName}] loadLatestDataInBatches: Successfully saved to localStorage`) + await tableDataStorage.setItem(cacheKey, dataToSave) + console.log(`[${tableName}] loadLatestDataInBatches: Successfully saved to IndexedDB`) // Verify it was saved - const saved = localStorage.getItem(cacheKey) + const saved = await tableDataStorage.getItem(cacheKey) if (saved) { - const parsed = JSON.parse(saved) - console.log(`[${tableName}] loadLatestDataInBatches: Verification - localStorage contains ${parsed.data?.length || 0} records`) + console.log(`[${tableName}] loadLatestDataInBatches: Verification - IndexedDB contains ${saved.data?.length || 0} records`) } } catch (error) { - console.error(`[${tableName}] loadLatestDataInBatches: Failed to persist data to localStorage:`, error) + console.error(`[${tableName}] loadLatestDataInBatches: Failed to persist data to IndexedDB:`, error) } } else { - console.log(`[${tableName}] loadLatestDataInBatches: Not on client, skipping localStorage`) + console.log(`[${tableName}] loadLatestDataInBatches: Not on client, skipping IndexedDB`) } if (onProgress) { diff --git a/nuxt4-app/app/utils/storage.ts b/nuxt4-app/app/utils/storage.ts new file mode 100644 index 0000000..2093e37 --- /dev/null +++ b/nuxt4-app/app/utils/storage.ts @@ -0,0 +1,305 @@ +/** + * Storage utility that uses IndexedDB for table data and localStorage for config/secrets + * + * Strategy: + * - IndexedDB: ALL table/datasource data (can store hundreds of MB) + * - localStorage: Only for secrets, variables, configurations (small data) + */ + +const DB_NAME = 'analitica-nucleo-db' +const DB_VERSION = 1 +const STORE_NAME = 'table-data' + +let dbInstance: IDBDatabase | null = null + +/** + * Initialize IndexedDB + */ +async function initDB(): Promise { + if (dbInstance) return dbInstance + + return new Promise((resolve, reject) => { + const request = indexedDB.open(DB_NAME, DB_VERSION) + + request.onerror = () => { + console.error('[Storage] IndexedDB error:', request.error) + reject(request.error) + } + + request.onsuccess = () => { + dbInstance = request.result + console.log('[Storage] IndexedDB initialized successfully') + resolve(dbInstance) + } + + request.onupgradeneeded = (event) => { + const db = (event.target as IDBOpenDBRequest).result + + // Create object store if it doesn't exist + if (!db.objectStoreNames.contains(STORE_NAME)) { + db.createObjectStore(STORE_NAME, { keyPath: 'key' }) + console.log('[Storage] Created object store:', STORE_NAME) + } + } + }) +} + +/** + * Deep clone to remove reactivity and make data serializable for IndexedDB + */ +function toPlainObject(obj: any): any { + // Use JSON parse/stringify to deep clone and remove all reactivity/proxies + try { + return JSON.parse(JSON.stringify(obj)) + } catch (error) { + console.error('[Storage] Failed to convert to plain object:', error) + return obj + } +} + +/** + * Save data to IndexedDB + */ +async function saveToIndexedDB(key: string, data: any): Promise { + try { + const db = await initDB() + + // Convert reactive objects to plain objects + const plainData = toPlainObject(data) + + return new Promise((resolve, reject) => { + const transaction = db.transaction([STORE_NAME], 'readwrite') + const store = transaction.objectStore(STORE_NAME) + + const request = store.put({ + key, + value: plainData, + timestamp: Date.now() + }) + + request.onsuccess = () => { + console.log(`[Storage] Saved to IndexedDB: ${key}`) + resolve(true) + } + + request.onerror = () => { + console.error(`[Storage] Failed to save to IndexedDB: ${key}`, request.error) + reject(request.error) + } + }) + } catch (error) { + console.error('[Storage] IndexedDB save error:', error) + return false + } +} + +/** + * Load data from IndexedDB + */ +async function loadFromIndexedDB(key: string): Promise { + try { + const db = await initDB() + + return new Promise((resolve, reject) => { + const transaction = db.transaction([STORE_NAME], 'readonly') + const store = transaction.objectStore(STORE_NAME) + const request = store.get(key) + + request.onsuccess = () => { + if (request.result) { + console.log(`[Storage] Loaded from IndexedDB: ${key}`) + resolve(request.result.value) + } else { + console.log(`[Storage] No data found in IndexedDB for: ${key}`) + resolve(null) + } + } + + request.onerror = () => { + console.error(`[Storage] Failed to load from IndexedDB: ${key}`, request.error) + reject(request.error) + } + }) + } catch (error) { + console.error('[Storage] IndexedDB load error:', error) + return null + } +} + +/** + * Remove data from IndexedDB + */ +async function removeFromIndexedDB(key: string): Promise { + try { + const db = await initDB() + + return new Promise((resolve, reject) => { + const transaction = db.transaction([STORE_NAME], 'readwrite') + const store = transaction.objectStore(STORE_NAME) + const request = store.delete(key) + + request.onsuccess = () => { + console.log(`[Storage] Removed from IndexedDB: ${key}`) + resolve(true) + } + + request.onerror = () => { + console.error(`[Storage] Failed to remove from IndexedDB: ${key}`, request.error) + reject(request.error) + } + }) + } catch (error) { + console.error('[Storage] IndexedDB remove error:', error) + return false + } +} + +/** + * Storage interface for table/datasource data (uses IndexedDB exclusively) + */ +export const tableDataStorage = { + /** + * Save table data to IndexedDB (for large datasets) + */ + async setItem(key: string, data: any): Promise { + if (!process.client) { + console.log('[TableDataStorage] Not on client, skipping storage') + return + } + + const dataString = JSON.stringify(data) + const dataSizeKB = new Blob([dataString]).size / 1024 + console.log(`[TableDataStorage] Saving ${dataSizeKB.toFixed(2)} KB to IndexedDB for key: ${key}`) + + try { + const success = await saveToIndexedDB(key, data) + if (success) { + console.log(`[TableDataStorage] ✓ Successfully saved ${dataSizeKB.toFixed(2)} KB to IndexedDB`) + return + } else { + throw new Error('Failed to save to IndexedDB') + } + } catch (error) { + console.error('[TableDataStorage] ✗ Failed to save to IndexedDB:', error) + throw new Error(`No se pudo guardar la tabla: ${error}`) + } + }, + + /** + * Load table data from IndexedDB + */ + async getItem(key: string): Promise { + if (!process.client) { + console.log('[TableDataStorage] Not on client, skipping storage') + return null + } + + console.log(`[TableDataStorage] Loading from IndexedDB: ${key}`) + + try { + const data = await loadFromIndexedDB(key) + if (data !== null) { + const dataString = JSON.stringify(data) + const dataSizeKB = new Blob([dataString]).size / 1024 + console.log(`[TableDataStorage] ✓ Loaded ${dataSizeKB.toFixed(2)} KB from IndexedDB`) + return data + } else { + console.log(`[TableDataStorage] No data found in IndexedDB for: ${key}`) + return null + } + } catch (error) { + console.error('[TableDataStorage] ✗ Failed to load from IndexedDB:', error) + return null + } + }, + + /** + * Remove table data from IndexedDB + */ + async removeItem(key: string): Promise { + if (!process.client) return + + console.log(`[TableDataStorage] Removing from IndexedDB: ${key}`) + + try { + await removeFromIndexedDB(key) + console.log(`[TableDataStorage] ✓ Removed from IndexedDB: ${key}`) + } catch (error) { + console.error('[TableDataStorage] ✗ Failed to remove from IndexedDB:', error) + } + } +} + +/** + * Storage interface for config/secrets (uses localStorage exclusively) + */ +export const configStorage = { + /** + * Save config/secrets to localStorage (for small data only) + */ + setItem(key: string, data: any): void { + if (!process.client) { + console.log('[ConfigStorage] Not on client, skipping storage') + return + } + + const dataString = JSON.stringify(data) + const dataSizeKB = new Blob([dataString]).size / 1024 + console.log(`[ConfigStorage] Saving ${dataSizeKB.toFixed(2)} KB to localStorage for key: ${key}`) + + try { + localStorage.setItem(key, dataString) + console.log(`[ConfigStorage] ✓ Saved to localStorage`) + } catch (error: any) { + if (error.name === 'QuotaExceededError') { + console.error('[ConfigStorage] ✗ localStorage quota exceeded') + throw new Error('No hay espacio en localStorage para guardar la configuración') + } + throw error + } + }, + + /** + * Load config/secrets from localStorage + */ + getItem(key: string): any | null { + if (!process.client) { + console.log('[ConfigStorage] Not on client, skipping storage') + return null + } + + console.log(`[ConfigStorage] Loading from localStorage: ${key}`) + + try { + const item = localStorage.getItem(key) + if (item) { + const data = JSON.parse(item) + const dataSizeKB = new Blob([item]).size / 1024 + console.log(`[ConfigStorage] ✓ Loaded ${dataSizeKB.toFixed(2)} KB from localStorage`) + return data + } else { + console.log(`[ConfigStorage] No data found in localStorage for: ${key}`) + return null + } + } catch (error) { + console.error('[ConfigStorage] ✗ Failed to load from localStorage:', error) + return null + } + }, + + /** + * Remove config/secrets from localStorage + */ + removeItem(key: string): void { + if (!process.client) return + + console.log(`[ConfigStorage] Removing from localStorage: ${key}`) + + try { + localStorage.removeItem(key) + console.log(`[ConfigStorage] ✓ Removed from localStorage: ${key}`) + } catch (error) { + console.error('[ConfigStorage] ✗ Failed to remove from localStorage:', error) + } + } +} \ No newline at end of file diff --git a/nuxt4-app/plugins/tableStores.client.ts b/nuxt4-app/plugins/tableStores.client.ts deleted file mode 100644 index 5b035d3..0000000 --- a/nuxt4-app/plugins/tableStores.client.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { useMetadataStore } from '~/stores/metadata' -import { createTableDataStore } from '~/stores/tableDataFactory' - -export default defineNuxtPlugin(async (nuxtApp) => { - // Wait for metadata to be available - const metadataStore = useMetadataStore() - - // Initialize metadata first - await metadataStore.initialize() - - // Create stores for all available tables - const tableStores = new Map>() - - metadataStore.allTables.forEach((table) => { - const storeName = table.table - const storeFactory = createTableDataStore(storeName, 100) - - // Register the store - tableStores.set(storeName, storeFactory) - - // Optionally initialize stores in the background (lazy loading) - // You can uncomment this to preload all data - // const storeInstance = storeFactory() - // storeInstance.loadFromCache() - }) - - // Provide access to table stores - return { - provide: { - tableStores, - // Helper function to get a table store - getTableStore: (tableName: string) => { - const storeFactory = tableStores.get(tableName) - if (!storeFactory) { - console.warn(`Table store for "${tableName}" not found`) - return null - } - return storeFactory() - } - } - } -}) \ No newline at end of file