This commit is contained in:
2025-09-30 02:02:56 -06:00
parent a346e30777
commit 1cf09ee640
7 changed files with 425 additions and 81 deletions

View File

@@ -124,6 +124,7 @@ const isLoadingAll = ref(false)
const loadingProgress = ref(0) const loadingProgress = ref(0)
// Get the table store for this specific datasource (using name, not table) // 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(() => { const tableStore = computed(() => {
if (typeof $getTableStore === 'function') { if (typeof $getTableStore === 'function') {
return $getTableStore(props.metadata.name) return $getTableStore(props.metadata.name)

View File

@@ -277,8 +277,12 @@ const totalRowCount = computed(() => {
}) })
// Methods // Methods
function selectTable(tableName: string) { async function selectTable(tableName: string) {
if (selectedTableName.value === tableName) return console.log(`[Explorer] selectTable called with: ${tableName}`)
if (selectedTableName.value === tableName) {
console.log(`[Explorer] Table ${tableName} already selected, skipping`)
return
}
selectedTableName.value = tableName selectedTableName.value = tableName
@@ -286,20 +290,31 @@ function selectTable(tableName: string) {
if (typeof $getTableStore === 'function') { if (typeof $getTableStore === 'function') {
const store = $getTableStore(tableName) const store = $getTableStore(tableName)
if (store) { if (store) {
console.log(`[Explorer] Got store for ${tableName} via plugin`)
currentTableStore.value = store 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 { } else {
// Fallback: create store directly // Fallback: create store directly
console.log(`[Explorer] Creating store for ${tableName} directly (fallback)`)
currentTableStore.value = useTableDataStore(tableName) 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() { async function refreshTableData() {
console.log('[Explorer] refreshTableData called')
if (currentTableStore.value) { if (currentTableStore.value) {
console.log(`[Explorer] Refreshing data for current table (${selectedTableName.value})`)
await currentTableStore.value.refreshData() 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 // Lifecycle
onMounted(async () => { onMounted(async () => {
console.log('[Explorer] onMounted: Initializing metadata store')
await metadataStore.initialize() await metadataStore.initialize()
// Auto-select first table if available // Auto-select first table if available
if (metadataStore.hasMetadata && !selectedTableName.value) { if (metadataStore.hasMetadata && !selectedTableName.value) {
const firstTable = metadataStore.allTables[0] const firstTable = metadataStore.allTables[0]
if (firstTable) { 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 // Auto-select first table when metadata becomes available
watch(() => metadataStore.hasMetadata, (hasMetadata) => { watch(() => metadataStore.hasMetadata, (hasMetadata) => {
if (hasMetadata && !selectedTableName.value && metadataStore.allTables.length > 0) { 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
} }
}) })
</script> </script>

View File

@@ -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<string, ReturnType<typeof createTableDataStore>>()
const storeInstances = new Map<string, ReturnType<ReturnType<typeof createTableDataStore>>>()
// 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
}
}
}
})

View File

@@ -1,4 +1,5 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { tableDataStorage } from '~/utils/storage'
export interface TableDataState<T = Record<string, unknown>> { export interface TableDataState<T = Record<string, unknown>> {
data: T[] data: T[]
@@ -23,8 +24,8 @@ export interface TableDataActions<T = Record<string, unknown>> {
loadData(force?: boolean): Promise<void> loadData(force?: boolean): Promise<void>
refreshData(): Promise<void> refreshData(): Promise<void>
fetchData(): Promise<void> fetchData(): Promise<void>
clearData(): void clearData(): Promise<void>
loadFromCache(): void loadFromCache(): Promise<void>
extractErrorMessage(error: unknown): string extractErrorMessage(error: unknown): string
initialize(): Promise<void> initialize(): Promise<void>
getRecord(id: string | number): T | undefined getRecord(id: string | number): T | undefined
@@ -169,16 +170,16 @@ export function createTableDataStore<T = Record<string, unknown>>(
this.lastUpdated = new Date().toISOString() this.lastUpdated = new Date().toISOString()
this.initialized = true this.initialized = true
// Persist to localStorage for offline access // Persist to IndexedDB for offline access
if (process.client) { if (process.client) {
try { try {
localStorage.setItem(cacheKey, JSON.stringify({ await tableDataStorage.setItem(cacheKey, {
data: this.data, data: this.data,
lastUpdated: this.lastUpdated, lastUpdated: this.lastUpdated,
limit: this.limit limit: this.limit
})) })
} catch (error) { } 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) { } catch (error: any) {
@@ -195,9 +196,9 @@ export function createTableDataStore<T = Record<string, unknown>>(
}, },
/** /**
* Load data from localStorage cache * Load data from IndexedDB cache
*/ */
loadFromCache(): void { async loadFromCache(): Promise<void> {
if (!process.client) { if (!process.client) {
console.log(`[${tableName}] loadFromCache: Not on client, skipping`) console.log(`[${tableName}] loadFromCache: Not on client, skipping`)
return return
@@ -205,9 +206,8 @@ export function createTableDataStore<T = Record<string, unknown>>(
try { try {
console.log(`[${tableName}] loadFromCache: Attempting to load from key: ${cacheKey}`) console.log(`[${tableName}] loadFromCache: Attempting to load from key: ${cacheKey}`)
const cached = localStorage.getItem(cacheKey) const parsedCache = await tableDataStorage.getItem(cacheKey)
if (cached) { if (parsedCache) {
const parsedCache = JSON.parse(cached)
console.log(`[${tableName}] loadFromCache: Found ${parsedCache.data?.length || 0} records in cache`) console.log(`[${tableName}] loadFromCache: Found ${parsedCache.data?.length || 0} records in cache`)
this.data = parsedCache.data || [] this.data = parsedCache.data || []
this.lastUpdated = parsedCache.lastUpdated || null this.lastUpdated = parsedCache.lastUpdated || null
@@ -225,7 +225,7 @@ export function createTableDataStore<T = Record<string, unknown>>(
/** /**
* Clear all data * Clear all data
*/ */
clearData(): void { async clearData(): Promise<void> {
this.data = [] this.data = []
this.error = null this.error = null
this.lastUpdated = null this.lastUpdated = null
@@ -233,7 +233,7 @@ export function createTableDataStore<T = Record<string, unknown>>(
if (process.client) { if (process.client) {
try { try {
localStorage.removeItem(cacheKey) await tableDataStorage.removeItem(cacheKey)
} catch (error) { } catch (error) {
console.warn(`Failed to clear ${tableName} data cache:`, error) console.warn(`Failed to clear ${tableName} data cache:`, error)
} }
@@ -260,7 +260,7 @@ export function createTableDataStore<T = Record<string, unknown>>(
*/ */
async initialize(): Promise<void> { async initialize(): Promise<void> {
// Load from cache first for immediate availability // Load from cache first for immediate availability
this.loadFromCache() await this.loadFromCache()
// Then try to fetch fresh data // Then try to fetch fresh data
await this.loadData() await this.loadData()
@@ -349,30 +349,29 @@ export function createTableDataStore<T = Record<string, unknown>>(
console.log(`[${tableName}] loadAllDataInBatches: Finished loading ${this.data.length} records`) console.log(`[${tableName}] loadAllDataInBatches: Finished loading ${this.data.length} records`)
// Persist to localStorage // Persist to IndexedDB
if (process.client) { if (process.client) {
try { try {
console.log(`[${tableName}] loadAllDataInBatches: Persisting to localStorage with key: ${cacheKey}`) console.log(`[${tableName}] loadAllDataInBatches: Persisting to IndexedDB with key: ${cacheKey}`)
const dataToSave = { const dataToSave = {
data: this.data, data: this.data,
lastUpdated: this.lastUpdated, lastUpdated: this.lastUpdated,
limit: this.limit limit: this.limit
} }
console.log(`[${tableName}] loadAllDataInBatches: Saving ${dataToSave.data.length} records`) console.log(`[${tableName}] loadAllDataInBatches: Saving ${dataToSave.data.length} records`)
localStorage.setItem(cacheKey, JSON.stringify(dataToSave)) await tableDataStorage.setItem(cacheKey, dataToSave)
console.log(`[${tableName}] loadAllDataInBatches: Successfully saved to localStorage`) console.log(`[${tableName}] loadAllDataInBatches: Successfully saved to IndexedDB`)
// Verify it was saved // Verify it was saved
const saved = localStorage.getItem(cacheKey) const saved = await tableDataStorage.getItem(cacheKey)
if (saved) { if (saved) {
const parsed = JSON.parse(saved) console.log(`[${tableName}] loadAllDataInBatches: Verification - IndexedDB contains ${saved.data?.length || 0} records`)
console.log(`[${tableName}] loadAllDataInBatches: Verification - localStorage contains ${parsed.data?.length || 0} records`)
} }
} catch (error) { } 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 { } else {
console.log(`[${tableName}] loadAllDataInBatches: Not on client, skipping localStorage`) console.log(`[${tableName}] loadAllDataInBatches: Not on client, skipping IndexedDB`)
} }
if (onProgress) { if (onProgress) {
@@ -466,30 +465,29 @@ export function createTableDataStore<T = Record<string, unknown>>(
this.lastUpdated = new Date().toISOString() this.lastUpdated = new Date().toISOString()
console.log(`[${tableName}] loadLatestDataInBatches: Added ${newRecordsCount} new records, total: ${this.data.length}`) console.log(`[${tableName}] loadLatestDataInBatches: Added ${newRecordsCount} new records, total: ${this.data.length}`)
// Persist to localStorage // Persist to IndexedDB
if (process.client) { if (process.client) {
try { try {
console.log(`[${tableName}] loadLatestDataInBatches: Persisting to localStorage with key: ${cacheKey}`) console.log(`[${tableName}] loadLatestDataInBatches: Persisting to IndexedDB with key: ${cacheKey}`)
const dataToSave = { const dataToSave = {
data: this.data, data: this.data,
lastUpdated: this.lastUpdated, lastUpdated: this.lastUpdated,
limit: this.limit limit: this.limit
} }
console.log(`[${tableName}] loadLatestDataInBatches: Saving ${dataToSave.data.length} records`) console.log(`[${tableName}] loadLatestDataInBatches: Saving ${dataToSave.data.length} records`)
localStorage.setItem(cacheKey, JSON.stringify(dataToSave)) await tableDataStorage.setItem(cacheKey, dataToSave)
console.log(`[${tableName}] loadLatestDataInBatches: Successfully saved to localStorage`) console.log(`[${tableName}] loadLatestDataInBatches: Successfully saved to IndexedDB`)
// Verify it was saved // Verify it was saved
const saved = localStorage.getItem(cacheKey) const saved = await tableDataStorage.getItem(cacheKey)
if (saved) { if (saved) {
const parsed = JSON.parse(saved) console.log(`[${tableName}] loadLatestDataInBatches: Verification - IndexedDB contains ${saved.data?.length || 0} records`)
console.log(`[${tableName}] loadLatestDataInBatches: Verification - localStorage contains ${parsed.data?.length || 0} records`)
} }
} catch (error) { } 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 { } else {
console.log(`[${tableName}] loadLatestDataInBatches: Not on client, skipping localStorage`) console.log(`[${tableName}] loadLatestDataInBatches: Not on client, skipping IndexedDB`)
} }
if (onProgress) { if (onProgress) {

View File

@@ -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<IDBDatabase> {
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<boolean> {
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<any | null> {
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<boolean> {
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<void> {
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<any | null> {
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<void> {
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)
}
}
}

View File

@@ -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<string, ReturnType<typeof createTableDataStore>>()
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()
}
}
}
})