diff --git a/METABASE_API_ENDPOINTS.md b/METABASE_API_ENDPOINTS.md new file mode 100644 index 0000000..ed5e8ea --- /dev/null +++ b/METABASE_API_ENDPOINTS.md @@ -0,0 +1,269 @@ +# 🔌 REPORTE DE ENDPOINTS API DE METABASE + +**Fecha:** 2025-10-14 +**Instancia:** metabase.nucleoriofrio.com +**Autenticación:** X-API-KEY header +**Formato:** JSON + +--- + +## 📋 ENDPOINTS PRINCIPALES + +### 🎯 **CARDS (Questions/Queries)** + +Estos son los endpoints MÁS IMPORTANTES para nuestro caso, ya que trabajaremos con las 9 queries documentadas. + +| Método | Endpoint | Descripción | +|--------|----------|-------------| +| `GET` | `/api/card` | Listar todas las cards/preguntas | +| `GET` | `/api/card/:id` | Obtener detalles de una card específica | +| `POST` | `/api/card` | Crear una nueva card | +| `PUT` | `/api/card/:id` | Actualizar una card existente | +| `DELETE` | `/api/card/:id` | Eliminar una card | +| `POST` | `/api/card/:id/query` | **⭐ EJECUTAR una query y obtener resultados** | +| `GET` | `/api/card/:id/query` | Ejecutar query con caché | +| `GET` | `/api/card/:id/query/:export-format` | Exportar resultados (csv, json, xlsx) | + +**💡 Endpoint clave:** `/api/card/:id/query` - Este lo usaremos para ejecutar las 9 queries con parámetros. + +--- + +### 🗄️ **DATABASE** + +| Método | Endpoint | Descripción | +|--------|----------|-------------| +| `GET` | `/api/database` | Listar todas las bases de datos | +| `GET` | `/api/database/:id` | Obtener detalles de una BD | +| `POST` | `/api/database` | Crear conexión a BD | +| `PUT` | `/api/database/:id` | Actualizar conexión | +| `POST` | `/api/database/:id/sync_schema` | Sincronizar esquema | +| `GET` | `/api/database/:id/metadata` | Obtener metadata de tablas/campos | +| `POST` | `/api/database/validate` | Validar conexión antes de guardar | + +--- + +### 📊 **DATASET (Queries Ad-hoc)** + +Endpoints para ejecutar queries SQL nativas sin crear cards. + +| Método | Endpoint | Descripción | +|--------|----------|-------------| +| `POST` | `/api/dataset` | **⭐ Ejecutar query SQL nativa directamente** | +| `POST` | `/api/dataset/:export-format` | Ejecutar y exportar (csv, json, xlsx) | + +**💡 Uso importante:** Podemos usar `/api/dataset` para probar queries SQL antes de crearlas como cards. + +**Ejemplo de payload:** +```json +{ + "database": 2, + "type": "native", + "native": { + "query": "SELECT * FROM vista_detalle_ingresos LIMIT 10", + "template-tags": {} + } +} +``` + +--- + +### 📁 **COLLECTION** + +| Método | Endpoint | Descripción | +|--------|----------|-------------| +| `GET` | `/api/collection` | Listar colecciones | +| `GET` | `/api/collection/:id` | Detalles de colección | +| `GET` | `/api/collection/:id/items` | Items dentro de colección | +| `POST` | `/api/collection` | Crear colección | +| `PUT` | `/api/collection/:id` | Actualizar colección | + +--- + +### 📈 **DASHBOARD** + +| Método | Endpoint | Descripción | +|--------|----------|-------------| +| `GET` | `/api/dashboard` | Listar dashboards | +| `GET` | `/api/dashboard/:id` | Detalles de dashboard | +| `POST` | `/api/dashboard` | Crear dashboard | +| `PUT` | `/api/dashboard/:id` | Actualizar dashboard | +| `POST` | `/api/dashboard/:id/cards` | Agregar card a dashboard | +| `GET` | `/api/dashboard/:id/params/:param-key/values` | Valores para parámetro | + +--- + +### 🔍 **TABLE & FIELD** + +| Método | Endpoint | Descripción | +|--------|----------|-------------| +| `GET` | `/api/table/:id` | Detalles de tabla | +| `GET` | `/api/table/:id/query_metadata` | Metadata completa de tabla | +| `GET` | `/api/field/:id` | Detalles de campo | +| `POST` | `/api/field/:id/values` | Valores únicos de un campo | + +--- + +### 👤 **USER & SESSION** + +| Método | Endpoint | Descripción | +|--------|----------|-------------| +| `GET` | `/api/user/current` | Usuario autenticado actual | +| `GET` | `/api/user` | Listar usuarios | +| `POST` | `/api/session` | Login (crear sesión) | +| `DELETE` | `/api/session` | Logout | + +--- + +### ⚙️ **SETTINGS** + +| Método | Endpoint | Descripción | +|--------|----------|-------------| +| `GET` | `/api/setting` | Obtener configuraciones | +| `PUT` | `/api/setting/:key` | Actualizar configuración | + +--- + +### 🔐 **PERMISSIONS** + +| Método | Endpoint | Descripción | +|--------|----------|-------------| +| `GET` | `/api/permissions/graph` | Ver árbol de permisos | +| `PUT` | `/api/permissions/graph` | Actualizar permisos | +| `GET` | `/api/permissions/group` | Listar grupos | + +--- + +### 📤 **UPLOAD** (Anteriormente CSV) + +| Método | Endpoint | Descripción | +|--------|----------|-------------| +| `POST` | `/api/upload/csv` | Subir CSV como tabla | + +--- + +### 📧 **NOTIFICATION** (Reemplaza Pulse/Alert) + +| Método | Endpoint | Descripción | +|--------|----------|-------------| +| `GET` | `/api/notification` | Listar notificaciones | +| `POST` | `/api/notification` | Crear notificación | +| `PUT` | `/api/notification/:id` | Actualizar notificación | + +--- + +### 🔧 **UTIL & ANALYTICS** + +| Método | Endpoint | Descripción | +|--------|----------|-------------| +| `GET` | `/api/analytics/anonymous-stats` | Estadísticas de uso (antes /util/stats) | +| `GET` | `/api/util/logs` | Logs del sistema | +| `POST` | `/api/util/password_check` | Validar contraseña | + +--- + +## 🎯 ENDPOINTS CRÍTICOS PARA NUESTRO PROYECTO + +### 1. **Ejecutar queries con parámetros** +```http +POST /api/card/:id/query +Content-Type: application/json +X-API-KEY: mb_bRFd1DTqU1eK1GeYFo3z0WhwKcqdA5qHNCXko3ZV6FU= + +{ + "parameters": [ + {"type": "date/single", "target": ["variable", ["template-tag", "fecha_desde"]], "value": "2025-01-01"}, + {"type": "date/single", "target": ["variable", ["template-tag", "fecha_hasta"]], "value": "2025-12-31"}, + {"type": "category", "target": ["variable", ["template-tag", "incluir_anulados"]], "value": false} + ] +} +``` + +### 2. **Ejecutar SQL nativa directamente** +```http +POST /api/dataset +Content-Type: application/json +X-API-KEY: mb_bRFd1DTqU1eK1GeYFo3z0WhwKcqdA5qHNCXko3ZV6FU= + +{ + "database": 2, + "type": "native", + "native": { + "query": "SELECT COALESCE(SUM(...), 0) as total FROM vista_detalle_ingresos WHERE ...", + "template-tags": { + "incluir_anulados": { + "type": "boolean", + "default": false + } + } + } +} +``` + +### 3. **Listar todas las cards para encontrar IDs** +```http +GET /api/card?f=all +X-API-KEY: mb_bRFd1DTqU1eK1GeYFo3z0WhwKcqdA5qHNCXko3ZV6FU= +``` + +### 4. **Obtener metadata de una card** +```http +GET /api/card/:id +X-API-KEY: mb_bRFd1DTqU1eK1GeYFo3z0WhwKcqdA5qHNCXko3ZV6FU= +``` + +--- + +## 📝 NOTAS IMPORTANTES + +1. **Autenticación:** Todos los requests requieren header `X-API-KEY: mb_bRFd1DTqU1eK1GeYFo3z0WhwKcqdA5qHNCXko3ZV6FU=` + +2. **Estructura de respuesta de queries:** +```json +{ + "data": { + "rows": [...], // Array de arrays con los valores + "cols": [...], // Metadata de columnas (nombre, tipo, etc) + "rows_truncated": 1000, // Límite de filas + "native_form": {...} // Query SQL ejecutada + }, + "database_id": 2, + "started_at": "...", + "json_query": {...}, + "average_execution_time": null, + "status": "completed", + "context": "ad-hoc", + "row_count": 1, + "running_time": 45 +} +``` + +3. **IDs de las 9 queries del Panorama:** + - Query 1: `/api/card/38/query` (panorama_totales_financieros_principales) + - Query 2-9: Por determinar + +4. **Parámetros template-tags:** + - `fecha_desde`: tipo `date/single`, opcional + - `fecha_hasta`: tipo `date/single`, opcional + - `incluir_anulados`: tipo `boolean`, default `false` + +5. **Testing de queries:** + - Usar `/api/dataset` para probar SQL antes de crear/editar cards + - Usar `/api/card/:id/query` para probar cards existentes con diferentes parámetros + +--- + +## 🚀 PRÓXIMOS PASOS + +1. ✅ Listar todas las cards con `GET /api/card?f=all` para identificar las 9 queries +2. ✅ Para cada query (1-9): + - Obtener metadata con `GET /api/card/:id` + - Ejecutar con parámetros default usando `POST /api/card/:id/query` + - Verificar estructura de respuesta + - Validar que coincida con la especificación en `METABASE_QUERIES_PANORAMA.md` +3. ✅ Ajustar queries que no devuelvan la estructura correcta + +--- + +**Documento creado:** 2025-10-14 +**Autor:** Claude Code +**Proyecto:** Analítica Núcleo - Exploración API Metabase diff --git a/nuxt4-app/app/components/metabase/MetabaseCardDisplay.vue b/nuxt4-app/app/components/metabase/MetabaseCardDisplay.vue new file mode 100644 index 0000000..81ba6ba --- /dev/null +++ b/nuxt4-app/app/components/metabase/MetabaseCardDisplay.vue @@ -0,0 +1,187 @@ + + + diff --git a/nuxt4-app/app/components/metabase/MetabaseCardsTable.vue b/nuxt4-app/app/components/metabase/MetabaseCardsTable.vue new file mode 100644 index 0000000..d2b2a1b --- /dev/null +++ b/nuxt4-app/app/components/metabase/MetabaseCardsTable.vue @@ -0,0 +1,266 @@ + + + diff --git a/nuxt4-app/app/pages/metabase-debug.vue b/nuxt4-app/app/pages/metabase-debug.vue new file mode 100644 index 0000000..ac274d6 --- /dev/null +++ b/nuxt4-app/app/pages/metabase-debug.vue @@ -0,0 +1,203 @@ + + + diff --git a/nuxt4-app/nuxt.config.ts b/nuxt4-app/nuxt.config.ts index 336adbd..fc151cd 100644 --- a/nuxt4-app/nuxt.config.ts +++ b/nuxt4-app/nuxt.config.ts @@ -144,6 +144,7 @@ export default defineNuxtConfig({ // Server-side only // Use container name for Docker internal communication metabaseUrl: process.env.METABASE_URL || 'http://metabase:3000', + metabaseApiKey: process.env.METABASE_API_KEY || '', metabaseEmail: process.env.METABASE_EMAIL || 'claudeCode0@nucleoriofrio.com', metabasePassword: process.env.METABASE_PASSWORD || 'vK^NyZdZDH#p', // Public (client + server) diff --git a/nuxt4-app/server/api/metabase/cards/[id]/index.get.ts b/nuxt4-app/server/api/metabase/cards/[id]/index.get.ts new file mode 100644 index 0000000..4492210 --- /dev/null +++ b/nuxt4-app/server/api/metabase/cards/[id]/index.get.ts @@ -0,0 +1,24 @@ +/** + * Get a specific Metabase card by ID + */ +export default defineEventHandler(async (event) => { + try { + const id = getRouterParam(event, 'id') + + if (!id) { + throw createError({ + statusCode: 400, + statusMessage: 'Card ID is required' + }) + } + + const card = await getMetabaseCard(parseInt(id)) + return card + } catch (error: any) { + console.error('[API] Failed to get Metabase card:', error) + throw createError({ + statusCode: error.statusCode || 500, + statusMessage: error.statusMessage || 'Failed to fetch card' + }) + } +}) diff --git a/nuxt4-app/server/api/metabase/cards/[id]/query.get.ts b/nuxt4-app/server/api/metabase/cards/[id]/query.get.ts new file mode 100644 index 0000000..a70f0dc --- /dev/null +++ b/nuxt4-app/server/api/metabase/cards/[id]/query.get.ts @@ -0,0 +1,24 @@ +/** + * Execute a Metabase card query with cache (GET) + */ +export default defineEventHandler(async (event) => { + try { + const id = getRouterParam(event, 'id') + + if (!id) { + throw createError({ + statusCode: 400, + statusMessage: 'Card ID is required' + }) + } + + const result = await executeCardQueryCached(parseInt(id)) + return result + } catch (error: any) { + console.error('[API] Failed to execute cached query:', error) + throw createError({ + statusCode: error.statusCode || 500, + statusMessage: error.statusMessage || 'Failed to execute cached query' + }) + } +}) diff --git a/nuxt4-app/server/api/metabase/cards/[id]/query.post.ts b/nuxt4-app/server/api/metabase/cards/[id]/query.post.ts new file mode 100644 index 0000000..1dbd83c --- /dev/null +++ b/nuxt4-app/server/api/metabase/cards/[id]/query.post.ts @@ -0,0 +1,27 @@ +/** + * Execute a Metabase card query (POST) + */ +export default defineEventHandler(async (event) => { + try { + const id = getRouterParam(event, 'id') + + if (!id) { + throw createError({ + statusCode: 400, + statusMessage: 'Card ID is required' + }) + } + + const body = await readBody(event) + const parameters = body?.parameters + + const result = await executeCardQuery(parseInt(id), parameters) + return result + } catch (error: any) { + console.error('[API] Failed to execute Metabase card query:', error) + throw createError({ + statusCode: error.statusCode || 500, + statusMessage: error.statusMessage || 'Failed to execute query' + }) + } +}) diff --git a/nuxt4-app/server/api/metabase/cards/index.get.ts b/nuxt4-app/server/api/metabase/cards/index.get.ts new file mode 100644 index 0000000..5db7329 --- /dev/null +++ b/nuxt4-app/server/api/metabase/cards/index.get.ts @@ -0,0 +1,18 @@ +/** + * Get all Metabase cards/questions + */ +export default defineEventHandler(async (event) => { + try { + const query = getQuery(event) + const filter = query.f as string | undefined + + const cards = await getMetabaseCards(filter) + return cards + } catch (error: any) { + console.error('[API] Failed to get Metabase cards:', error) + throw createError({ + statusCode: error.statusCode || 500, + statusMessage: error.statusMessage || 'Failed to fetch cards' + }) + } +}) diff --git a/nuxt4-app/server/utils/metabase.ts b/nuxt4-app/server/utils/metabase.ts index 41b7912..0971bdd 100644 --- a/nuxt4-app/server/utils/metabase.ts +++ b/nuxt4-app/server/utils/metabase.ts @@ -6,6 +6,7 @@ const config = useRuntimeConfig() const METABASE_URL = config.metabaseUrl || 'http://metabase:3000' +const METABASE_API_KEY = config.metabaseApiKey || '' const METABASE_EMAIL = config.metabaseEmail || 'claudeCode0@nucleoriofrio.com' const METABASE_PASSWORD = config.metabasePassword || 'vK^NyZdZDH#p' @@ -50,29 +51,37 @@ export async function getMetabaseToken(): Promise { /** * Make an authenticated request to Metabase API + * Supports both API Key and Session Token authentication */ export async function metabaseFetch( endpoint: string, options: RequestInit = {} ): Promise { - const token = await getMetabaseToken() + const headers: Record = { + 'Content-Type': 'application/json', + ...options.headers as Record + } + + // Prefer API Key if available + if (METABASE_API_KEY) { + headers['X-API-KEY'] = METABASE_API_KEY + } else { + const token = await getMetabaseToken() + headers['X-Metabase-Session'] = token + } try { const response = await $fetch(`${METABASE_URL}${endpoint}`, { ...options, - headers: { - ...options.headers, - 'X-Metabase-Session': token, - 'Content-Type': 'application/json' - } + headers }) return response } catch (error: any) { console.error(`[Metabase] Request failed for ${endpoint}:`, error) - // If token is invalid, clear it and retry once - if (error.statusCode === 401) { + // If using session token and it's invalid, clear it and retry once + if (error.statusCode === 401 && !METABASE_API_KEY) { console.log('[Metabase] Token invalid, clearing and retrying...') sessionToken = null tokenExpiry = 0 @@ -126,3 +135,36 @@ export async function queryMetabaseTable(databaseId: number, tableId: number, qu } }) } + +/** + * Get all cards/questions + */ +export async function getMetabaseCards(filter?: string) { + const endpoint = filter ? `/api/card?f=${filter}` : '/api/card' + return metabaseFetch(endpoint) +} + +/** + * Get a specific card by ID + */ +export async function getMetabaseCard(cardId: number) { + return metabaseFetch(`/api/card/${cardId}`) +} + +/** + * Execute a card query with optional parameters + */ +export async function executeCardQuery(cardId: number, parameters?: any[]) { + const body = parameters ? { parameters } : {} + return metabaseFetch(`/api/card/${cardId}/query`, { + method: 'POST', + body + }) +} + +/** + * Execute a card query with cache (GET) + */ +export async function executeCardQueryCached(cardId: number) { + return metabaseFetch(`/api/card/${cardId}/query`) +}