From 4b25a70b8b0d5e2fdf6c3c5629671bbb7dfcc736 Mon Sep 17 00:00:00 2001 From: josedario87 Date: Mon, 13 Oct 2025 17:46:48 -0600 Subject: [PATCH] feat: add Metabase API integration Add complete integration with Metabase API to fetch data from 'facturador supabase' database. Features: - Server-side Metabase authentication using session tokens - Utility functions for Metabase API requests with auto-retry - API endpoints to proxy Metabase requests - GET /api/metabase/databases - List all databases - GET /api/metabase/tables/:databaseId - Get tables and metadata - POST /api/metabase/query - Execute queries against tables - useMetabase() composable for frontend consumption - getDatabases() - Fetch available databases - getDatabaseMetadata() - Get tables and fields info - queryTable() - Execute queries with filters and limits - resultToObjects() - Helper to convert results to objects Session tokens are cached and auto-refreshed when expired. This enables the application to display real data from the facturador database without using embeds or iframes. --- nuxt4-app/app/composables/useMetabase.ts | 154 ++++++++++++++++++ .../server/api/metabase/databases.get.ts | 15 ++ nuxt4-app/server/api/metabase/query.post.ts | 30 ++++ .../api/metabase/tables/[databaseId].get.ts | 24 +++ nuxt4-app/server/utils/metabase.ts | 127 +++++++++++++++ 5 files changed, 350 insertions(+) create mode 100644 nuxt4-app/app/composables/useMetabase.ts create mode 100644 nuxt4-app/server/api/metabase/databases.get.ts create mode 100644 nuxt4-app/server/api/metabase/query.post.ts create mode 100644 nuxt4-app/server/api/metabase/tables/[databaseId].get.ts create mode 100644 nuxt4-app/server/utils/metabase.ts diff --git a/nuxt4-app/app/composables/useMetabase.ts b/nuxt4-app/app/composables/useMetabase.ts new file mode 100644 index 0000000..33471a0 --- /dev/null +++ b/nuxt4-app/app/composables/useMetabase.ts @@ -0,0 +1,154 @@ +/** + * Composable for interacting with Metabase API + * + * Provides methods to query databases, tables, and execute queries + * through the Nuxt server API endpoints + */ + +export interface MetabaseDatabase { + id: number + name: string + engine: string + [key: string]: any +} + +export interface MetabaseTable { + id: number + name: string + display_name: string + schema: string + fields: MetabaseField[] + [key: string]: any +} + +export interface MetabaseField { + id: number + name: string + display_name: string + base_type: string + semantic_type: string | null + [key: string]: any +} + +export interface MetabaseQueryResult { + data: { + rows: any[][] + cols: Array<{ + name: string + display_name: string + base_type: string + [key: string]: any + }> + } + row_count: number + status: string + [key: string]: any +} + +export function useMetabase() { + const loading = ref(false) + const error = ref(null) + + /** + * Get all available databases + */ + async function getDatabases(): Promise { + loading.value = true + error.value = null + + try { + const databases = await $fetch('/api/metabase/databases') + return databases + } catch (err: any) { + error.value = err + console.error('[useMetabase] Failed to get databases:', err) + throw err + } finally { + loading.value = false + } + } + + /** + * Get metadata for a specific database (includes all tables and fields) + */ + async function getDatabaseMetadata(databaseId: number): Promise { + loading.value = true + error.value = null + + try { + const metadata = await $fetch(`/api/metabase/tables/${databaseId}`) + return metadata + } catch (err: any) { + error.value = err + console.error(`[useMetabase] Failed to get metadata for database ${databaseId}:`, err) + throw err + } finally { + loading.value = false + } + } + + /** + * Execute a query against a specific table + * + * @param databaseId - The Metabase database ID + * @param tableId - The Metabase table ID + * @param query - Optional query parameters (limit, filters, etc.) + */ + async function queryTable( + databaseId: number, + tableId: number, + query: { + limit?: number + filter?: any[] + 'order-by'?: any[] + [key: string]: any + } = {} + ): Promise { + loading.value = true + error.value = null + + try { + const result = await $fetch('/api/metabase/query', { + method: 'POST', + body: { + databaseId, + tableId, + query + } + }) + return result + } catch (err: any) { + error.value = err + console.error('[useMetabase] Failed to execute query:', err) + throw err + } finally { + loading.value = false + } + } + + /** + * Helper to convert Metabase query result to array of objects + * Makes it easier to work with the data in components + */ + function resultToObjects(result: MetabaseQueryResult): Record[] { + const cols = result.data.cols + const rows = result.data.rows + + return rows.map(row => { + const obj: Record = {} + cols.forEach((col, index) => { + obj[col.name] = row[index] + }) + return obj + }) + } + + return { + loading, + error, + getDatabases, + getDatabaseMetadata, + queryTable, + resultToObjects + } +} diff --git a/nuxt4-app/server/api/metabase/databases.get.ts b/nuxt4-app/server/api/metabase/databases.get.ts new file mode 100644 index 0000000..f7a73a2 --- /dev/null +++ b/nuxt4-app/server/api/metabase/databases.get.ts @@ -0,0 +1,15 @@ +/** + * Get list of databases from Metabase + */ +export default defineEventHandler(async (event) => { + try { + const databases = await getMetabaseDatabases() + return databases + } catch (error: any) { + console.error('[API] Failed to get Metabase databases:', error) + throw createError({ + statusCode: error.statusCode || 500, + statusMessage: error.statusMessage || 'Failed to fetch databases' + }) + } +}) diff --git a/nuxt4-app/server/api/metabase/query.post.ts b/nuxt4-app/server/api/metabase/query.post.ts new file mode 100644 index 0000000..718ee12 --- /dev/null +++ b/nuxt4-app/server/api/metabase/query.post.ts @@ -0,0 +1,30 @@ +/** + * Execute a query against Metabase + */ +export default defineEventHandler(async (event) => { + const body = await readBody(event) + + const { databaseId, tableId, query } = body + + if (!databaseId || !tableId) { + throw createError({ + statusCode: 400, + statusMessage: 'Database ID and Table ID are required' + }) + } + + try { + const result = await queryMetabaseTable( + parseInt(databaseId), + parseInt(tableId), + query + ) + return result + } catch (error: any) { + console.error('[API] Failed to execute Metabase query:', error) + throw createError({ + statusCode: error.statusCode || 500, + statusMessage: error.statusMessage || 'Failed to execute query' + }) + } +}) diff --git a/nuxt4-app/server/api/metabase/tables/[databaseId].get.ts b/nuxt4-app/server/api/metabase/tables/[databaseId].get.ts new file mode 100644 index 0000000..8f2f32f --- /dev/null +++ b/nuxt4-app/server/api/metabase/tables/[databaseId].get.ts @@ -0,0 +1,24 @@ +/** + * Get tables from a specific Metabase database + */ +export default defineEventHandler(async (event) => { + const databaseId = getRouterParam(event, 'databaseId') + + if (!databaseId) { + throw createError({ + statusCode: 400, + statusMessage: 'Database ID is required' + }) + } + + try { + const metadata = await getMetabaseTables(parseInt(databaseId)) + return metadata + } catch (error: any) { + console.error(`[API] Failed to get tables for database ${databaseId}:`, error) + throw createError({ + statusCode: error.statusCode || 500, + statusMessage: error.statusMessage || 'Failed to fetch tables' + }) + } +}) diff --git a/nuxt4-app/server/utils/metabase.ts b/nuxt4-app/server/utils/metabase.ts new file mode 100644 index 0000000..f894230 --- /dev/null +++ b/nuxt4-app/server/utils/metabase.ts @@ -0,0 +1,127 @@ +/** + * Metabase API utility + * + * Handles authentication and requests to Metabase API + */ + +const METABASE_URL = 'https://metabase.nucleoriofrio.com' +const METABASE_EMAIL = 'claudeCode0@nucleoriofrio.com' +const METABASE_PASSWORD = 'vK^NyZdZDH#p' + +let sessionToken: string | null = null +let tokenExpiry: number = 0 + +/** + * Get a valid Metabase session token + * Reuses existing token if still valid, otherwise requests a new one + */ +export async function getMetabaseToken(): Promise { + // Check if we have a valid token + if (sessionToken && Date.now() < tokenExpiry) { + return sessionToken + } + + // Request a new token + try { + const response = await $fetch<{ id: string }>(`${METABASE_URL}/api/session`, { + method: 'POST', + body: { + username: METABASE_EMAIL, + password: METABASE_PASSWORD + } + }) + + sessionToken = response.id + + // Tokens expire after 14 days by default, but we'll refresh after 13 days to be safe + tokenExpiry = Date.now() + (13 * 24 * 60 * 60 * 1000) + + console.log('[Metabase] New session token obtained') + return sessionToken + } catch (error) { + console.error('[Metabase] Failed to obtain session token:', error) + throw createError({ + statusCode: 500, + statusMessage: 'Failed to authenticate with Metabase' + }) + } +} + +/** + * Make an authenticated request to Metabase API + */ +export async function metabaseFetch( + endpoint: string, + options: RequestInit = {} +): Promise { + const token = await getMetabaseToken() + + try { + const response = await $fetch(`${METABASE_URL}${endpoint}`, { + ...options, + headers: { + ...options.headers, + 'X-Metabase-Session': token, + 'Content-Type': 'application/json' + } + }) + + 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) { + console.log('[Metabase] Token invalid, clearing and retrying...') + sessionToken = null + tokenExpiry = 0 + + // Retry once with fresh token + const newToken = await getMetabaseToken() + return await $fetch(`${METABASE_URL}${endpoint}`, { + ...options, + headers: { + ...options.headers, + 'X-Metabase-Session': newToken, + 'Content-Type': 'application/json' + } + }) + } + + throw createError({ + statusCode: error.statusCode || 500, + statusMessage: error.message || 'Metabase request failed' + }) + } +} + +/** + * Get database metadata + */ +export async function getMetabaseDatabases() { + return metabaseFetch('/api/database') +} + +/** + * Get tables from a specific database + */ +export async function getMetabaseTables(databaseId: number) { + return metabaseFetch(`/api/database/${databaseId}/metadata`) +} + +/** + * Execute a query against a table + */ +export async function queryMetabaseTable(databaseId: number, tableId: number, query: any = {}) { + return metabaseFetch('/api/dataset', { + method: 'POST', + body: { + database: databaseId, + type: 'query', + query: { + 'source-table': tableId, + ...query + } + } + }) +}