Files
analiticaNucleo/nuxt4-app/server/services/table-service.ts

287 lines
8.5 KiB
TypeScript

import { getTableConfig, tableNames } from '../data-sources'
import type { ParsedQuery, TableConfig } from '../data-sources/types'
import { getSupabaseClient } from '../utils/supabase'
import { applyParsedQuery } from './query-runner'
type GenericObject = Record<string, unknown>
function isRecord(value: unknown): value is GenericObject {
return typeof value === 'object' && value !== null && !Array.isArray(value)
}
interface MetadataOptions {
parsedQuery?: ParsedQuery | null
}
interface DataFilters {
id?: string
createdFrom?: string
createdTo?: string
}
interface DataOptions {
parsedQuery?: ParsedQuery | null
filters?: DataFilters
limit?: number
offset?: number
}
function serializeRow(row: unknown, config: TableConfig): GenericObject | null {
if (!isRecord(row) || ('error' in row && (row as any).error === true)) {
return null
}
const transform = config.transforms?.serializeRow
return transform ? transform(row) : row
}
export async function fetchAllTablesMetadata() {
const results = await Promise.allSettled(
tableNames.map((name) => fetchTableMetadata(name))
)
// Log any errors for debugging
results.forEach((result, index) => {
if (result.status === 'rejected') {
console.error(`Failed to fetch metadata for table ${tableNames[index]}:`, result.reason)
}
})
return results
.filter((result): result is PromiseFulfilledResult<Awaited<ReturnType<typeof fetchTableMetadata>>> =>
result.status === 'fulfilled'
)
.map((result) => result.value)
}
export async function fetchTableMetadata(tableName: string, options?: MetadataOptions) {
try {
const config = getTableConfig(tableName)
if (!config) {
throw createError({ statusCode: 404, statusMessage: `Tabla ${tableName} no encontrada` })
}
const supabase = getSupabaseClient()
const baseSelect = config.defaultSelect ?? '*'
const samplePromise = applyParsedQuery(
supabase.from(config.table).select(baseSelect),
options?.parsedQuery ?? null
)
.limit(1)
// Try to get created_at range, but don't fail if the column doesn't exist
const earliestPromise = supabase
.from(config.table)
.select('created_at')
.order('created_at', { ascending: true })
.limit(1)
const latestPromise = supabase
.from(config.table)
.select('created_at')
.order('created_at', { ascending: false })
.limit(1)
const [sampleResult, earliestResult, latestResult] =
await Promise.all([samplePromise, earliestPromise, latestPromise])
const { data: sampleData, error: sampleError } = sampleResult
// Handle created_at queries that may fail for views without this column
const earliest = earliestResult.error ? { data: null } : earliestResult
const latest = latestResult.error ? { data: null } : latestResult
if (sampleError) {
console.error(`Error fetching sample for ${config.table}:`, {
message: sampleError.message,
details: sampleError.details,
hint: sampleError.hint,
code: sampleError.code,
full: sampleError
})
const errorMsg = sampleError.message || sampleError.hint || sampleError.details || sampleError.code || 'Error desconocido al obtener muestra'
throw createError({ statusCode: 500, statusMessage: errorMsg })
}
const sampleSet = Array.isArray(sampleData) ? sampleData : []
const sampleRow = serializeRow(sampleSet[0] ?? null, config)
const columnNames = sampleRow ? Object.keys(sampleRow) : []
const approxRowSize = sampleRow ? JSON.stringify(sampleRow).length : 0
const approxSizeBytes = null // We'll calculate this from in-memory data
return {
name: config.name,
table: config.table,
primaryKey: config.primaryKey ?? 'id',
rowCount: 0, // Will be calculated from in-memory data
approxSizeBytes,
columns: columnNames,
createdAtRange: {
from: earliest.data?.[0]?.created_at ?? null,
to: latest.data?.[0]?.created_at ?? null
},
sampleRow,
lastRefreshed: new Date().toISOString(),
description: config.description
}
} catch (error: any) {
console.error(`Unexpected error in fetchTableMetadata for ${tableName}:`, error)
throw createError({
statusCode: 500,
statusMessage: error?.message || error?.toString() || 'Error inesperado al obtener metadatos'
})
}
}
export async function fetchTableRecordMetadata(tableName: string, id: string) {
const config = getTableConfig(tableName)
if (!config) {
throw createError({ statusCode: 404, statusMessage: `Tabla ${tableName} no encontrada` })
}
const supabase = getSupabaseClient()
const primaryKey = config.primaryKey ?? 'id'
const { data, error } = await supabase
.from(config.table)
.select(config.defaultSelect ?? '*')
.eq(primaryKey, id)
.maybeSingle()
if (error) {
throw createError({ statusCode: 500, statusMessage: error.message })
}
if (!data || (typeof data === 'object' && data !== null && 'error' in data)) {
if (data && typeof data === 'object' && 'message' in data) {
throw createError({ statusCode: 500, statusMessage: String((data as any).message) })
}
throw createError({ statusCode: 404, statusMessage: `Registro ${id} no encontrado` })
}
return {
table: config.table,
id,
metadata: serializeRow(data, config)
}
}
export async function fetchTableRecord(tableName: string, id: string) {
const config = getTableConfig(tableName)
if (!config) {
throw createError({ statusCode: 404, statusMessage: `Tabla ${tableName} no encontrada` })
}
const supabase = getSupabaseClient()
const primaryKey = config.primaryKey ?? 'id'
const { data, error } = await supabase
.from(config.table)
.select(config.defaultSelect ?? '*')
.eq(primaryKey, id)
.maybeSingle()
if (error) {
throw createError({ statusCode: 500, statusMessage: error.message })
}
if (!data || (typeof data === 'object' && data !== null && 'error' in data)) {
if (data && typeof data === 'object' && 'message' in data) {
throw createError({ statusCode: 500, statusMessage: String((data as any).message) })
}
throw createError({ statusCode: 404, statusMessage: `Registro ${id} no encontrado` })
}
return serializeRow(data, config)
}
export async function fetchAllData(limitPerTable = 100) {
const supabase = getSupabaseClient()
const tablePromises = tableNames.map(async (name) => {
const config = getTableConfig(name)!
const { data, error, count } = await supabase
.from(config.table)
.select(config.defaultSelect ?? '*', { count: 'exact' })
.limit(limitPerTable)
if (error) {
throw createError({ statusCode: 500, statusMessage: error.message })
}
const rows = Array.isArray(data) ? data : []
return {
table: config.table,
count: count ?? 0,
limit: limitPerTable,
records: rows
.map((row) => serializeRow(row, config))
.filter((row): row is GenericObject => row !== null)
}
})
return Promise.all(tablePromises)
}
export async function fetchTableData(tableName: string, options?: DataOptions) {
const config = getTableConfig(tableName)
if (!config) {
throw createError({ statusCode: 404, statusMessage: `Tabla ${tableName} no encontrada` })
}
const supabase = getSupabaseClient()
const primaryKey = config.primaryKey ?? 'id'
const limit = options?.limit ?? 100
let query = supabase
.from(config.table)
.select(config.defaultSelect ?? '*', { count: 'exact' })
query = applyParsedQuery(query as never, options?.parsedQuery ?? null) as never
if (options?.filters?.id) {
query = query.eq(primaryKey, options.filters.id)
}
if (options?.filters?.createdFrom) {
query = query.gte('created_at', options.filters.createdFrom)
}
if (options?.filters?.createdTo) {
query = query.lte('created_at', options.filters.createdTo)
}
if (!options?.parsedQuery?.limit) {
query = query.limit(limit)
}
if (options?.offset !== undefined && options.offset > 0) {
query = query.range(options.offset, options.offset + limit - 1)
}
const { data, error, count } = await query
if (error) {
throw createError({ statusCode: 500, statusMessage: error.message })
}
const rows = Array.isArray(data) ? data : []
const records = rows
.map((row) => serializeRow(row, config))
.filter((row): row is GenericObject => row !== null)
return {
table: config.table,
count: count ?? records.length,
limit,
records
}
}