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.
This commit is contained in:
154
nuxt4-app/app/composables/useMetabase.ts
Normal file
154
nuxt4-app/app/composables/useMetabase.ts
Normal 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
|
||||
}
|
||||
}
|
||||
15
nuxt4-app/server/api/metabase/databases.get.ts
Normal file
15
nuxt4-app/server/api/metabase/databases.get.ts
Normal 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'
|
||||
})
|
||||
}
|
||||
})
|
||||
30
nuxt4-app/server/api/metabase/query.post.ts
Normal file
30
nuxt4-app/server/api/metabase/query.post.ts
Normal 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'
|
||||
})
|
||||
}
|
||||
})
|
||||
24
nuxt4-app/server/api/metabase/tables/[databaseId].get.ts
Normal file
24
nuxt4-app/server/api/metabase/tables/[databaseId].get.ts
Normal 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'
|
||||
})
|
||||
}
|
||||
})
|
||||
127
nuxt4-app/server/utils/metabase.ts
Normal file
127
nuxt4-app/server/utils/metabase.ts
Normal 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
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user