feat: add Metabase API integration
All checks were successful
build-and-deploy / build (push) Successful in 45s
build-and-deploy / deploy (push) Successful in 3s

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.
This commit is contained in:
2025-10-13 17:46:48 -06:00
parent f4047e1505
commit 4b25a70b8b
5 changed files with 350 additions and 0 deletions

View File

@@ -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<Error | null>(null)
/**
* Get all available databases
*/
async function getDatabases(): Promise<MetabaseDatabase[]> {
loading.value = true
error.value = null
try {
const databases = await $fetch<MetabaseDatabase[]>('/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<any> {
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<MetabaseQueryResult> {
loading.value = true
error.value = null
try {
const result = await $fetch<MetabaseQueryResult>('/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<string, any>[] {
const cols = result.data.cols
const rows = result.data.rows
return rows.map(row => {
const obj: Record<string, any> = {}
cols.forEach((col, index) => {
obj[col.name] = row[index]
})
return obj
})
}
return {
loading,
error,
getDatabases,
getDatabaseMetadata,
queryTable,
resultToObjects
}
}

View File

@@ -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'
})
}
})

View File

@@ -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'
})
}
})

View File

@@ -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'
})
}
})

View File

@@ -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<string> {
// 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<T = any>(
endpoint: string,
options: RequestInit = {}
): Promise<T> {
const token = await getMetabaseToken()
try {
const response = await $fetch<T>(`${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<T>(`${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
}
}
})
}