feat: funciones Go para API Metabase y tipo MetabaseClient
Añade funciones Go stub para la API de Metabase en dominio infra: auth, CRUD de cards, dashboards y users, execute_query y execute_card. Incluye tipo MetabaseClient y helper HTTP compartido. Todas las funciones son impuras con stubs not-implemented.
This commit is contained in:
@@ -0,0 +1,49 @@
|
|||||||
|
package infra
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MetabaseAuth autentica con email y password contra una instancia Metabase.
|
||||||
|
// Retorna un MetabaseClient con el session token listo para usar.
|
||||||
|
// baseURL es la URL base sin trailing slash (ej: "http://localhost:3000").
|
||||||
|
func MetabaseAuth(baseURL, email, password string) (MetabaseClient, error) {
|
||||||
|
payload, _ := json.Marshal(map[string]string{
|
||||||
|
"username": email,
|
||||||
|
"password": password,
|
||||||
|
})
|
||||||
|
|
||||||
|
resp, err := http.Post(baseURL+"/api/session", "application/json", bytes.NewReader(payload))
|
||||||
|
if err != nil {
|
||||||
|
return MetabaseClient{}, fmt.Errorf("metabase auth: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return MetabaseClient{}, fmt.Errorf("read auth response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
return MetabaseClient{}, fmt.Errorf("metabase auth: status %d: %s", resp.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
var result struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(body, &result); err != nil {
|
||||||
|
return MetabaseClient{}, fmt.Errorf("parse auth response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return MetabaseClient{BaseURL: baseURL, Token: result.ID}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MetabaseNewClient crea un MetabaseClient usando una API key en lugar de session token.
|
||||||
|
// Las API keys se crean en Settings > Authentication > API Keys del admin de Metabase.
|
||||||
|
func MetabaseNewClient(baseURL, apiKey string) MetabaseClient {
|
||||||
|
return MetabaseClient{BaseURL: baseURL, Token: apiKey}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
---
|
||||||
|
name: metabase_auth
|
||||||
|
kind: function
|
||||||
|
lang: go
|
||||||
|
domain: infra
|
||||||
|
version: "1.0.0"
|
||||||
|
purity: impure
|
||||||
|
signature: "func MetabaseAuth(baseURL, email, password string) (MetabaseClient, error)"
|
||||||
|
description: "Autentica contra la API de Metabase con email y password. Retorna un MetabaseClient con session token valido por 14 dias (configurable con MAX_SESSION_AGE en Metabase). Endpoint: POST /api/session."
|
||||||
|
tags: [metabase, auth, session, api]
|
||||||
|
uses_functions: []
|
||||||
|
uses_types: [MetabaseClient_go_infra]
|
||||||
|
returns: [MetabaseClient_go_infra]
|
||||||
|
returns_optional: false
|
||||||
|
error_type: "error_go_core"
|
||||||
|
imports: [bytes, encoding/json, fmt, io, net/http]
|
||||||
|
tested: false
|
||||||
|
tests: []
|
||||||
|
test_file_path: ""
|
||||||
|
file_path: "functions/infra/metabase_auth.go"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ejemplo
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Autenticar con credenciales
|
||||||
|
client, err := MetabaseAuth("http://localhost:3000", "admin@example.com", "password123")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
// client.Token contiene el session token
|
||||||
|
|
||||||
|
// Alternativa: usar API key directamente
|
||||||
|
client := MetabaseNewClient("http://localhost:3000", "mb_api_key_xxxxx")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notas
|
||||||
|
|
||||||
|
Dos formas de obtener un MetabaseClient:
|
||||||
|
- `MetabaseAuth`: login con email/password, obtiene session token via POST /api/session. Token expira en 14 dias por defecto.
|
||||||
|
- `MetabaseNewClient`: usa una API key creada en el admin UI. No expira. Recomendado para automatizacion.
|
||||||
|
|
||||||
|
El token se envia como header `X-Metabase-Session` en todas las llamadas subsiguientes.
|
||||||
|
|
||||||
|
### Para un LLM que use estas funciones
|
||||||
|
|
||||||
|
1. Primero obtener un client con `MetabaseAuth()` o `MetabaseNewClient()`
|
||||||
|
2. Pasar el client a todas las funciones CRUD (usuarios, cards, dashboards)
|
||||||
|
3. Si recibes error 401, el token expiro — re-autenticar
|
||||||
|
4. Rate limiting: Metabase limita intentos de login fallidos
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package infra
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// MetabaseCreateCard crea una nueva card/pregunta en Metabase.
|
||||||
|
// name: nombre de la pregunta (obligatorio).
|
||||||
|
// datasetQuery: query de la card (obligatorio). Estructura:
|
||||||
|
//
|
||||||
|
// SQL nativo: {"database": 1, "type": "native", "native": {"query": "SELECT ..."}}
|
||||||
|
// MBQL: {"database": 1, "type": "query", "query": {"source-table": 4, ...}}
|
||||||
|
//
|
||||||
|
// display: tipo de visualizacion ("table", "bar", "line", "pie", "scalar", etc.).
|
||||||
|
// collectionID: ID de la coleccion/carpeta (0 = root).
|
||||||
|
// description: descripcion opcional (vacio = sin descripcion).
|
||||||
|
func MetabaseCreateCard(client MetabaseClient, name string, datasetQuery map[string]any, display string, collectionID int, description string) (map[string]any, error) {
|
||||||
|
body := map[string]any{
|
||||||
|
"name": name,
|
||||||
|
"dataset_query": datasetQuery,
|
||||||
|
"display": display,
|
||||||
|
"visualization_settings": map[string]any{},
|
||||||
|
}
|
||||||
|
if collectionID > 0 {
|
||||||
|
body["collection_id"] = collectionID
|
||||||
|
}
|
||||||
|
if description != "" {
|
||||||
|
body["description"] = description
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := metabaseRequest("POST", client.BaseURL, client.Token, "/api/card", body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("metabase create card: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
---
|
||||||
|
name: metabase_create_card
|
||||||
|
kind: function
|
||||||
|
lang: go
|
||||||
|
domain: infra
|
||||||
|
version: "1.0.0"
|
||||||
|
purity: impure
|
||||||
|
signature: "func MetabaseCreateCard(client MetabaseClient, name string, datasetQuery map[string]any, display string, collectionID int, description string) (map[string]any, error)"
|
||||||
|
description: "Crea una nueva card/pregunta en Metabase con query SQL nativa o MBQL. Endpoint: POST /api/card."
|
||||||
|
tags: [metabase, card, question, create, api]
|
||||||
|
uses_functions: []
|
||||||
|
uses_types: [MetabaseClient_go_infra]
|
||||||
|
returns: []
|
||||||
|
returns_optional: false
|
||||||
|
error_type: "error_go_core"
|
||||||
|
imports: [fmt]
|
||||||
|
tested: false
|
||||||
|
tests: []
|
||||||
|
test_file_path: ""
|
||||||
|
file_path: "functions/infra/metabase_create_card.go"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ejemplo
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Crear pregunta con SQL nativo
|
||||||
|
card, err := MetabaseCreateCard(client, "Revenue by Month", map[string]any{
|
||||||
|
"database": 1,
|
||||||
|
"type": "native",
|
||||||
|
"native": map[string]any{
|
||||||
|
"query": "SELECT date_trunc('month', created_at) as month, SUM(total) as revenue FROM orders GROUP BY 1 ORDER BY 1",
|
||||||
|
},
|
||||||
|
}, "line", 5, "Monthly revenue trend")
|
||||||
|
|
||||||
|
// Crear pregunta con MBQL (structured query)
|
||||||
|
card, err := MetabaseCreateCard(client, "Order Count", map[string]any{
|
||||||
|
"database": 1,
|
||||||
|
"type": "query",
|
||||||
|
"query": map[string]any{
|
||||||
|
"source-table": 4,
|
||||||
|
"aggregation": []any{[]any{"count"}},
|
||||||
|
},
|
||||||
|
}, "scalar", 0, "Total number of orders")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notas
|
||||||
|
|
||||||
|
### Parametros para un LLM
|
||||||
|
|
||||||
|
| Parametro | Tipo | Requerido | Descripcion |
|
||||||
|
|-----------|------|-----------|-------------|
|
||||||
|
| client | MetabaseClient | si | Cliente autenticado |
|
||||||
|
| name | string | si | Nombre de la pregunta |
|
||||||
|
| datasetQuery | map[string]any | si | Query. Ver estructura abajo |
|
||||||
|
| display | string | si | Tipo de visualizacion |
|
||||||
|
| collectionID | int | no | ID de coleccion. 0 = root collection |
|
||||||
|
| description | string | no | Descripcion. Vacio = sin descripcion |
|
||||||
|
|
||||||
|
### Estructura de datasetQuery
|
||||||
|
|
||||||
|
**SQL nativo:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"database": <database_id>,
|
||||||
|
"type": "native",
|
||||||
|
"native": {"query": "SELECT ..."}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**MBQL (structured):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"database": <database_id>,
|
||||||
|
"type": "query",
|
||||||
|
"query": {
|
||||||
|
"source-table": <table_id>,
|
||||||
|
"aggregation": [["count"]],
|
||||||
|
"breakout": [["field", <field_id>, {"temporal-unit": "month"}]],
|
||||||
|
"filter": ["=", ["field", <field_id>, null], "value"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Valores de display
|
||||||
|
|
||||||
|
table, bar, line, pie, scalar, area, row, combo, funnel, map, scatter, waterfall, progress, gauge
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package infra
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// MetabaseCreateDashboard crea un nuevo dashboard en Metabase.
|
||||||
|
// name: nombre del dashboard (obligatorio).
|
||||||
|
// description: descripcion opcional (vacio = sin descripcion).
|
||||||
|
// collectionID: ID de la coleccion/carpeta (0 = root).
|
||||||
|
func MetabaseCreateDashboard(client MetabaseClient, name, description string, collectionID int) (map[string]any, error) {
|
||||||
|
body := map[string]any{
|
||||||
|
"name": name,
|
||||||
|
}
|
||||||
|
if description != "" {
|
||||||
|
body["description"] = description
|
||||||
|
}
|
||||||
|
if collectionID > 0 {
|
||||||
|
body["collection_id"] = collectionID
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := metabaseRequest("POST", client.BaseURL, client.Token, "/api/dashboard", body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("metabase create dashboard: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
---
|
||||||
|
name: metabase_create_dashboard
|
||||||
|
kind: function
|
||||||
|
lang: go
|
||||||
|
domain: infra
|
||||||
|
version: "1.0.0"
|
||||||
|
purity: impure
|
||||||
|
signature: "func MetabaseCreateDashboard(client MetabaseClient, name, description string, collectionID int) (map[string]any, error)"
|
||||||
|
description: "Crea un nuevo dashboard vacio en Metabase. Para agregar cards usar MetabaseUpdateDashboard con el campo dashcards. Endpoint: POST /api/dashboard."
|
||||||
|
tags: [metabase, dashboard, create, api]
|
||||||
|
uses_functions: []
|
||||||
|
uses_types: [MetabaseClient_go_infra]
|
||||||
|
returns: []
|
||||||
|
returns_optional: false
|
||||||
|
error_type: "error_go_core"
|
||||||
|
imports: [fmt]
|
||||||
|
tested: false
|
||||||
|
tests: []
|
||||||
|
test_file_path: ""
|
||||||
|
file_path: "functions/infra/metabase_create_dashboard.go"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ejemplo
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Crear dashboard vacio
|
||||||
|
dashboard, err := MetabaseCreateDashboard(client, "Sales Overview", "KPIs de ventas", 5)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
dashboardID := int(dashboard["id"].(float64))
|
||||||
|
|
||||||
|
// Luego agregar cards con MetabaseUpdateDashboard
|
||||||
|
MetabaseUpdateDashboard(client, dashboardID, map[string]any{
|
||||||
|
"dashcards": []map[string]any{
|
||||||
|
{"id": -1, "card_id": 42, "size_x": 6, "size_y": 4, "col": 0, "row": 0},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notas
|
||||||
|
|
||||||
|
### Parametros para un LLM
|
||||||
|
|
||||||
|
| Parametro | Tipo | Requerido | Descripcion |
|
||||||
|
|-----------|------|-----------|-------------|
|
||||||
|
| client | MetabaseClient | si | Cliente autenticado |
|
||||||
|
| name | string | si | Nombre del dashboard |
|
||||||
|
| description | string | no | Descripcion. Vacio = sin descripcion |
|
||||||
|
| collectionID | int | no | Coleccion destino. 0 = root |
|
||||||
|
|
||||||
|
El dashboard se crea vacio. Para agregar cards, usar MetabaseUpdateDashboard con el array dashcards.
|
||||||
|
Retorna el objeto dashboard creado.
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package infra
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// MetabaseCreateUser crea un nuevo usuario en Metabase.
|
||||||
|
// firstName, lastName y email son obligatorios.
|
||||||
|
// password es opcional: si esta vacio, Metabase envia email de invitacion.
|
||||||
|
// groupIDs es opcional: IDs de grupos a asignar (nil = solo grupo default).
|
||||||
|
func MetabaseCreateUser(client MetabaseClient, firstName, lastName, email, password string, groupIDs []int) (map[string]any, error) {
|
||||||
|
body := map[string]any{
|
||||||
|
"first_name": firstName,
|
||||||
|
"last_name": lastName,
|
||||||
|
"email": email,
|
||||||
|
}
|
||||||
|
if password != "" {
|
||||||
|
body["password"] = password
|
||||||
|
}
|
||||||
|
if len(groupIDs) > 0 {
|
||||||
|
body["group_ids"] = groupIDs
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := metabaseRequest("POST", client.BaseURL, client.Token, "/api/user", body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("metabase create user: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
---
|
||||||
|
name: metabase_create_user
|
||||||
|
kind: function
|
||||||
|
lang: go
|
||||||
|
domain: infra
|
||||||
|
version: "1.0.0"
|
||||||
|
purity: impure
|
||||||
|
signature: "func MetabaseCreateUser(client MetabaseClient, firstName, lastName, email, password string, groupIDs []int) (map[string]any, error)"
|
||||||
|
description: "Crea un nuevo usuario en Metabase. Si no se provee password, Metabase envia email de invitacion. Requiere permisos de superusuario. Endpoint: POST /api/user."
|
||||||
|
tags: [metabase, user, create, api]
|
||||||
|
uses_functions: []
|
||||||
|
uses_types: [MetabaseClient_go_infra]
|
||||||
|
returns: []
|
||||||
|
returns_optional: false
|
||||||
|
error_type: "error_go_core"
|
||||||
|
imports: [fmt]
|
||||||
|
tested: false
|
||||||
|
tests: []
|
||||||
|
test_file_path: ""
|
||||||
|
file_path: "functions/infra/metabase_create_user.go"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ejemplo
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Crear usuario con password
|
||||||
|
user, err := MetabaseCreateUser(client, "John", "Doe", "john@example.com", "securePass123", nil)
|
||||||
|
|
||||||
|
// Crear usuario sin password (envia invitacion por email)
|
||||||
|
user, err := MetabaseCreateUser(client, "Jane", "Smith", "jane@example.com", "", []int{1, 3})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notas
|
||||||
|
|
||||||
|
### Parametros para un LLM
|
||||||
|
|
||||||
|
| Parametro | Tipo | Requerido | Descripcion |
|
||||||
|
|-----------|------|-----------|-------------|
|
||||||
|
| client | MetabaseClient | si | Cliente autenticado con permisos admin |
|
||||||
|
| firstName | string | si | Nombre del usuario |
|
||||||
|
| lastName | string | si | Apellido del usuario |
|
||||||
|
| email | string | si | Email unico del usuario |
|
||||||
|
| password | string | no | Password. Vacio = Metabase envia invitacion |
|
||||||
|
| groupIDs | []int | no | IDs de grupos. nil = solo grupo default |
|
||||||
|
|
||||||
|
El email debe ser unico. Si ya existe, retorna error 400.
|
||||||
|
Retorna el objeto usuario creado como map (mismos campos que MetabaseGetUser).
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package infra
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// MetabaseDeactivateUser desactiva (soft-delete) un usuario en Metabase.
|
||||||
|
// El usuario no se elimina permanentemente, solo se marca como inactivo.
|
||||||
|
// Requiere permisos de superusuario.
|
||||||
|
func MetabaseDeactivateUser(client MetabaseClient, userID int) error {
|
||||||
|
path := fmt.Sprintf("/api/user/%d", userID)
|
||||||
|
|
||||||
|
_, err := metabaseRequest("DELETE", client.BaseURL, client.Token, path, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("metabase deactivate user %d: %w", userID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
---
|
||||||
|
name: metabase_deactivate_user
|
||||||
|
kind: function
|
||||||
|
lang: go
|
||||||
|
domain: infra
|
||||||
|
version: "1.0.0"
|
||||||
|
purity: impure
|
||||||
|
signature: "func MetabaseDeactivateUser(client MetabaseClient, userID int) error"
|
||||||
|
description: "Desactiva (soft-delete) un usuario en Metabase. El usuario no se elimina permanentemente, solo se marca como inactivo. Para reactivar, usar PUT /api/user/:id/reactivate. Endpoint: DELETE /api/user/:id."
|
||||||
|
tags: [metabase, user, delete, deactivate, api]
|
||||||
|
uses_functions: []
|
||||||
|
uses_types: [MetabaseClient_go_infra]
|
||||||
|
returns: []
|
||||||
|
returns_optional: false
|
||||||
|
error_type: "error_go_core"
|
||||||
|
imports: [fmt]
|
||||||
|
tested: false
|
||||||
|
tests: []
|
||||||
|
test_file_path: ""
|
||||||
|
file_path: "functions/infra/metabase_deactivate_user.go"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ejemplo
|
||||||
|
|
||||||
|
```go
|
||||||
|
err := MetabaseDeactivateUser(client, 5)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
// Usuario 5 ahora esta inactivo
|
||||||
|
// Para ver desactivados: MetabaseListUsers(client, "deactivated", "", 0, 0)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notas
|
||||||
|
|
||||||
|
Es un soft-delete: el usuario se desactiva pero no se borra. Se puede reactivar con PUT /api/user/:id/reactivate.
|
||||||
|
|
||||||
|
Para listar usuarios desactivados, usar `MetabaseListUsers` con status "deactivated".
|
||||||
|
|
||||||
|
Requiere permisos de superusuario. Error 403 si no eres admin.
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package infra
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// MetabaseDeleteCard elimina permanentemente una card/pregunta de Metabase.
|
||||||
|
// Para soft-delete, usar MetabaseUpdateCard con archived: true.
|
||||||
|
func MetabaseDeleteCard(client MetabaseClient, cardID int) error {
|
||||||
|
path := fmt.Sprintf("/api/card/%d", cardID)
|
||||||
|
|
||||||
|
_, err := metabaseRequest("DELETE", client.BaseURL, client.Token, path, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("metabase delete card %d: %w", cardID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
---
|
||||||
|
name: metabase_delete_card
|
||||||
|
kind: function
|
||||||
|
lang: go
|
||||||
|
domain: infra
|
||||||
|
version: "1.0.0"
|
||||||
|
purity: impure
|
||||||
|
signature: "func MetabaseDeleteCard(client MetabaseClient, cardID int) error"
|
||||||
|
description: "Elimina permanentemente una card/pregunta de Metabase. Accion irreversible. Para soft-delete usar MetabaseUpdateCard con archived:true. Endpoint: DELETE /api/card/:id."
|
||||||
|
tags: [metabase, card, question, delete, api]
|
||||||
|
uses_functions: []
|
||||||
|
uses_types: [MetabaseClient_go_infra]
|
||||||
|
returns: []
|
||||||
|
returns_optional: false
|
||||||
|
error_type: "error_go_core"
|
||||||
|
imports: [fmt]
|
||||||
|
tested: false
|
||||||
|
tests: []
|
||||||
|
test_file_path: ""
|
||||||
|
file_path: "functions/infra/metabase_delete_card.go"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ejemplo
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Eliminar permanentemente
|
||||||
|
err := MetabaseDeleteCard(client, 42)
|
||||||
|
|
||||||
|
// Preferir soft-delete cuando sea posible:
|
||||||
|
// MetabaseUpdateCard(client, 42, map[string]any{"archived": true})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notas
|
||||||
|
|
||||||
|
**ATENCION**: Esta operacion es irreversible. La card se elimina permanentemente.
|
||||||
|
|
||||||
|
Para un borrado seguro, preferir archivar con `MetabaseUpdateCard(client, cardID, map[string]any{"archived": true})` que permite recuperar la card despues.
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package infra
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// MetabaseDeleteDashboard elimina permanentemente un dashboard de Metabase.
|
||||||
|
// Para soft-delete, usar MetabaseUpdateDashboard con archived: true.
|
||||||
|
func MetabaseDeleteDashboard(client MetabaseClient, dashboardID int) error {
|
||||||
|
path := fmt.Sprintf("/api/dashboard/%d", dashboardID)
|
||||||
|
|
||||||
|
_, err := metabaseRequest("DELETE", client.BaseURL, client.Token, path, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("metabase delete dashboard %d: %w", dashboardID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
---
|
||||||
|
name: metabase_delete_dashboard
|
||||||
|
kind: function
|
||||||
|
lang: go
|
||||||
|
domain: infra
|
||||||
|
version: "1.0.0"
|
||||||
|
purity: impure
|
||||||
|
signature: "func MetabaseDeleteDashboard(client MetabaseClient, dashboardID int) error"
|
||||||
|
description: "Elimina permanentemente un dashboard de Metabase. Accion irreversible. Para soft-delete usar MetabaseUpdateDashboard con archived:true. Endpoint: DELETE /api/dashboard/:id."
|
||||||
|
tags: [metabase, dashboard, delete, api]
|
||||||
|
uses_functions: []
|
||||||
|
uses_types: [MetabaseClient_go_infra]
|
||||||
|
returns: []
|
||||||
|
returns_optional: false
|
||||||
|
error_type: "error_go_core"
|
||||||
|
imports: [fmt]
|
||||||
|
tested: false
|
||||||
|
tests: []
|
||||||
|
test_file_path: ""
|
||||||
|
file_path: "functions/infra/metabase_delete_dashboard.go"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ejemplo
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Eliminar permanentemente
|
||||||
|
err := MetabaseDeleteDashboard(client, 1)
|
||||||
|
|
||||||
|
// Preferir soft-delete:
|
||||||
|
// MetabaseUpdateDashboard(client, 1, map[string]any{"archived": true})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notas
|
||||||
|
|
||||||
|
**ATENCION**: Esta operacion es irreversible. El dashboard y todas sus dashcards se eliminan permanentemente.
|
||||||
|
|
||||||
|
Para un borrado seguro, preferir archivar con `MetabaseUpdateDashboard(client, dashboardID, map[string]any{"archived": true})`.
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package infra
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// MetabaseExecuteCard ejecuta la query de una card/pregunta guardada.
|
||||||
|
// parameters: parametros de la query (nil si no tiene parametros).
|
||||||
|
// Retorna los resultados con columnas y filas.
|
||||||
|
func MetabaseExecuteCard(client MetabaseClient, cardID int, parameters []map[string]any) (map[string]any, error) {
|
||||||
|
path := fmt.Sprintf("/api/card/%d/query", cardID)
|
||||||
|
|
||||||
|
var body map[string]any
|
||||||
|
if len(parameters) > 0 {
|
||||||
|
body = map[string]any{"parameters": parameters}
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := metabaseRequest("POST", client.BaseURL, client.Token, path, body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("metabase execute card %d: %w", cardID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
---
|
||||||
|
name: metabase_execute_card
|
||||||
|
kind: function
|
||||||
|
lang: go
|
||||||
|
domain: infra
|
||||||
|
version: "1.0.0"
|
||||||
|
purity: impure
|
||||||
|
signature: "func MetabaseExecuteCard(client MetabaseClient, cardID int, parameters []map[string]any) (map[string]any, error)"
|
||||||
|
description: "Ejecuta la query de una card/pregunta guardada en Metabase y retorna los resultados. Soporta parametros para queries parametrizadas. Endpoint: POST /api/card/:id/query."
|
||||||
|
tags: [metabase, card, question, execute, query, api]
|
||||||
|
uses_functions: []
|
||||||
|
uses_types: [MetabaseClient_go_infra]
|
||||||
|
returns: []
|
||||||
|
returns_optional: false
|
||||||
|
error_type: "error_go_core"
|
||||||
|
imports: [fmt]
|
||||||
|
tested: false
|
||||||
|
tests: []
|
||||||
|
test_file_path: ""
|
||||||
|
file_path: "functions/infra/metabase_execute_card.go"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ejemplo
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Ejecutar sin parametros
|
||||||
|
result, err := MetabaseExecuteCard(client, 42, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
data := result["data"].(map[string]any)
|
||||||
|
rows := data["rows"].([]any)
|
||||||
|
fmt.Printf("Filas: %d\n", len(rows))
|
||||||
|
|
||||||
|
// Ejecutar con parametros
|
||||||
|
result, err := MetabaseExecuteCard(client, 42, []map[string]any{
|
||||||
|
{
|
||||||
|
"type": "category",
|
||||||
|
"target": []any{"variable", []any{"template-tag", "status"}},
|
||||||
|
"value": "active",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notas
|
||||||
|
|
||||||
|
### Estructura de la respuesta
|
||||||
|
|
||||||
|
| Campo | Tipo | Descripcion |
|
||||||
|
|-------|------|-------------|
|
||||||
|
| status | string | "completed" o "failed" |
|
||||||
|
| row_count | float64 | Numero de filas |
|
||||||
|
| running_time | float64 | Tiempo de ejecucion en ms |
|
||||||
|
| data.columns | []string | Nombres de columnas |
|
||||||
|
| data.rows | [][]any | Filas de datos |
|
||||||
|
| data.cols | []map | Metadata de columnas (name, base_type, display_name) |
|
||||||
|
| data.native_form.query | string | SQL ejecutado |
|
||||||
|
|
||||||
|
### Parametros para queries parametrizadas
|
||||||
|
|
||||||
|
```go
|
||||||
|
[]map[string]any{
|
||||||
|
{
|
||||||
|
"type": "category", // tipo del parametro
|
||||||
|
"target": []any{"variable", []any{"template-tag", "tag"}}, // referencia al template-tag
|
||||||
|
"value": "valor", // valor a inyectar
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Limite por defecto: 2000 filas. Para queries ad-hoc sin card, usar MetabaseExecuteQuery.
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package infra
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// MetabaseExecuteQuery ejecuta una query ad-hoc (sin guardar como card) en Metabase.
|
||||||
|
// databaseID: ID de la base de datos en Metabase.
|
||||||
|
// sql: query SQL a ejecutar.
|
||||||
|
// maxResults: limite de filas (0 = default 2000 de Metabase).
|
||||||
|
func MetabaseExecuteQuery(client MetabaseClient, databaseID int, sql string, maxResults int) (map[string]any, error) {
|
||||||
|
body := map[string]any{
|
||||||
|
"database": databaseID,
|
||||||
|
"type": "native",
|
||||||
|
"native": map[string]any{"query": sql},
|
||||||
|
}
|
||||||
|
if maxResults > 0 {
|
||||||
|
body["constraints"] = map[string]any{
|
||||||
|
"max-results": maxResults,
|
||||||
|
"max-results-bare-rows": maxResults,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := metabaseRequest("POST", client.BaseURL, client.Token, "/api/dataset", body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("metabase execute query: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
---
|
||||||
|
name: metabase_execute_query
|
||||||
|
kind: function
|
||||||
|
lang: go
|
||||||
|
domain: infra
|
||||||
|
version: "1.0.0"
|
||||||
|
purity: impure
|
||||||
|
signature: "func MetabaseExecuteQuery(client MetabaseClient, databaseID int, sql string, maxResults int) (map[string]any, error)"
|
||||||
|
description: "Ejecuta una query SQL ad-hoc contra una database de Metabase sin guardarla como card. Util para consultas rapidas y exploracion. Endpoint: POST /api/dataset."
|
||||||
|
tags: [metabase, query, execute, sql, dataset, api]
|
||||||
|
uses_functions: []
|
||||||
|
uses_types: [MetabaseClient_go_infra]
|
||||||
|
returns: []
|
||||||
|
returns_optional: false
|
||||||
|
error_type: "error_go_core"
|
||||||
|
imports: [fmt]
|
||||||
|
tested: false
|
||||||
|
tests: []
|
||||||
|
test_file_path: ""
|
||||||
|
file_path: "functions/infra/metabase_execute_query.go"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ejemplo
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Query simple
|
||||||
|
result, err := MetabaseExecuteQuery(client, 1, "SELECT * FROM users LIMIT 10", 0)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
data := result["data"].(map[string]any)
|
||||||
|
rows := data["rows"].([]any)
|
||||||
|
|
||||||
|
// Query con limite custom
|
||||||
|
result, err := MetabaseExecuteQuery(client, 1, "SELECT * FROM orders", 5000)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notas
|
||||||
|
|
||||||
|
### Parametros para un LLM
|
||||||
|
|
||||||
|
| Parametro | Tipo | Requerido | Descripcion |
|
||||||
|
|-----------|------|-----------|-------------|
|
||||||
|
| client | MetabaseClient | si | Cliente autenticado |
|
||||||
|
| databaseID | int | si | ID de la database en Metabase (obtener con GET /api/database) |
|
||||||
|
| sql | string | si | Query SQL a ejecutar |
|
||||||
|
| maxResults | int | no | Limite de filas. 0 = default 2000 |
|
||||||
|
|
||||||
|
### Diferencia con MetabaseExecuteCard
|
||||||
|
|
||||||
|
- `MetabaseExecuteQuery`: query ad-hoc, no se guarda. Usa POST /api/dataset.
|
||||||
|
- `MetabaseExecuteCard`: ejecuta una card ya guardada. Usa POST /api/card/:id/query.
|
||||||
|
|
||||||
|
Usar esta funcion para exploracion rapida. Si la query se va a reutilizar, crear una card con MetabaseCreateCard.
|
||||||
|
|
||||||
|
### Estructura de la respuesta
|
||||||
|
|
||||||
|
Misma estructura que MetabaseExecuteCard:
|
||||||
|
- `data.columns`: nombres de columnas
|
||||||
|
- `data.rows`: filas de datos
|
||||||
|
- `row_count`: numero de filas
|
||||||
|
- `running_time`: tiempo en ms
|
||||||
|
- `status`: "completed" o "failed"
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package infra
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// MetabaseGetCard obtiene una card/pregunta de Metabase por su ID.
|
||||||
|
func MetabaseGetCard(client MetabaseClient, cardID int) (map[string]any, error) {
|
||||||
|
path := fmt.Sprintf("/api/card/%d", cardID)
|
||||||
|
|
||||||
|
result, err := metabaseRequest("GET", client.BaseURL, client.Token, path, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("metabase get card %d: %w", cardID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
---
|
||||||
|
name: metabase_get_card
|
||||||
|
kind: function
|
||||||
|
lang: go
|
||||||
|
domain: infra
|
||||||
|
version: "1.0.0"
|
||||||
|
purity: impure
|
||||||
|
signature: "func MetabaseGetCard(client MetabaseClient, cardID int) (map[string]any, error)"
|
||||||
|
description: "Obtiene los detalles completos de una card/pregunta de Metabase por su ID. Incluye la query, visualizacion y metadata. Endpoint: GET /api/card/:id."
|
||||||
|
tags: [metabase, card, question, get, api]
|
||||||
|
uses_functions: []
|
||||||
|
uses_types: [MetabaseClient_go_infra]
|
||||||
|
returns: []
|
||||||
|
returns_optional: false
|
||||||
|
error_type: "error_go_core"
|
||||||
|
imports: [fmt]
|
||||||
|
tested: false
|
||||||
|
tests: []
|
||||||
|
test_file_path: ""
|
||||||
|
file_path: "functions/infra/metabase_get_card.go"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ejemplo
|
||||||
|
|
||||||
|
```go
|
||||||
|
card, err := MetabaseGetCard(client, 42)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
fmt.Println(card["name"], card["display"])
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notas
|
||||||
|
|
||||||
|
Retorna el objeto card completo. Error 404 si no existe.
|
||||||
|
|
||||||
|
### Campos principales
|
||||||
|
|
||||||
|
| Campo | Tipo | Descripcion |
|
||||||
|
|-------|------|-------------|
|
||||||
|
| id | float64 | ID de la card |
|
||||||
|
| name | string | Nombre |
|
||||||
|
| description | string | Descripcion |
|
||||||
|
| display | string | Tipo visualizacion |
|
||||||
|
| dataset_query | map | Query (native.query para SQL, query para MBQL) |
|
||||||
|
| visualization_settings | map | Config de visualizacion |
|
||||||
|
| collection_id | float64 | Coleccion contenedora |
|
||||||
|
| database_id | float64 | Database asociada |
|
||||||
|
| archived | bool | Archivada |
|
||||||
|
| creator | map | Objeto del usuario creador |
|
||||||
|
| created_at | string | Fecha creacion |
|
||||||
|
| updated_at | string | Fecha actualizacion |
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package infra
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// MetabaseGetDashboard obtiene un dashboard completo de Metabase incluyendo sus cards.
|
||||||
|
func MetabaseGetDashboard(client MetabaseClient, dashboardID int) (map[string]any, error) {
|
||||||
|
path := fmt.Sprintf("/api/dashboard/%d", dashboardID)
|
||||||
|
|
||||||
|
result, err := metabaseRequest("GET", client.BaseURL, client.Token, path, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("metabase get dashboard %d: %w", dashboardID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
---
|
||||||
|
name: metabase_get_dashboard
|
||||||
|
kind: function
|
||||||
|
lang: go
|
||||||
|
domain: infra
|
||||||
|
version: "1.0.0"
|
||||||
|
purity: impure
|
||||||
|
signature: "func MetabaseGetDashboard(client MetabaseClient, dashboardID int) (map[string]any, error)"
|
||||||
|
description: "Obtiene un dashboard completo de Metabase incluyendo todas sus dashcards (cards posicionadas en el dashboard), tabs y parametros. Endpoint: GET /api/dashboard/:id."
|
||||||
|
tags: [metabase, dashboard, get, api]
|
||||||
|
uses_functions: []
|
||||||
|
uses_types: [MetabaseClient_go_infra]
|
||||||
|
returns: []
|
||||||
|
returns_optional: false
|
||||||
|
error_type: "error_go_core"
|
||||||
|
imports: [fmt]
|
||||||
|
tested: false
|
||||||
|
tests: []
|
||||||
|
test_file_path: ""
|
||||||
|
file_path: "functions/infra/metabase_get_dashboard.go"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ejemplo
|
||||||
|
|
||||||
|
```go
|
||||||
|
dashboard, err := MetabaseGetDashboard(client, 1)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
fmt.Println(dashboard["name"])
|
||||||
|
|
||||||
|
// Acceder a las cards del dashboard
|
||||||
|
dashcards := dashboard["dashcards"].([]any)
|
||||||
|
for _, dc := range dashcards {
|
||||||
|
card := dc.(map[string]any)
|
||||||
|
fmt.Printf("Card ID: %v, Position: (%v, %v)\n",
|
||||||
|
card["card_id"], card["col"], card["row"])
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notas
|
||||||
|
|
||||||
|
### Campos principales
|
||||||
|
|
||||||
|
| Campo | Tipo | Descripcion |
|
||||||
|
|-------|------|-------------|
|
||||||
|
| id | float64 | ID del dashboard |
|
||||||
|
| name | string | Nombre |
|
||||||
|
| description | string | Descripcion |
|
||||||
|
| dashcards | []map | Array de dashcards (cards posicionadas) |
|
||||||
|
| parameters | []map | Filtros del dashboard |
|
||||||
|
| tabs | []map | Tabs del dashboard |
|
||||||
|
| collection_id | float64 | Coleccion contenedora |
|
||||||
|
| archived | bool | Archivado |
|
||||||
|
|
||||||
|
### Estructura de cada dashcard
|
||||||
|
|
||||||
|
| Campo | Tipo | Descripcion |
|
||||||
|
|-------|------|-------------|
|
||||||
|
| id | float64 | ID del dashcard (positivo) |
|
||||||
|
| card_id | float64 | ID de la card/pregunta asociada |
|
||||||
|
| card | map | Objeto card completo |
|
||||||
|
| size_x | float64 | Ancho en grid (1-18) |
|
||||||
|
| size_y | float64 | Alto en grid |
|
||||||
|
| col | float64 | Columna en grid (0-based) |
|
||||||
|
| row | float64 | Fila en grid (0-based) |
|
||||||
|
| dashboard_tab_id | float64 | Tab al que pertenece (null = sin tabs) |
|
||||||
|
| parameter_mappings | []map | Mapeo de filtros a la card |
|
||||||
|
| visualization_settings | map | Settings de visualizacion |
|
||||||
|
|
||||||
|
Usar estos datos para construir el payload de MetabaseUpdateDashboard.
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package infra
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// MetabaseGetUser obtiene un usuario de Metabase por su ID.
|
||||||
|
func MetabaseGetUser(client MetabaseClient, userID int) (map[string]any, error) {
|
||||||
|
path := fmt.Sprintf("/api/user/%d", userID)
|
||||||
|
|
||||||
|
result, err := metabaseRequest("GET", client.BaseURL, client.Token, path, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("metabase get user %d: %w", userID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
---
|
||||||
|
name: metabase_get_user
|
||||||
|
kind: function
|
||||||
|
lang: go
|
||||||
|
domain: infra
|
||||||
|
version: "1.0.0"
|
||||||
|
purity: impure
|
||||||
|
signature: "func MetabaseGetUser(client MetabaseClient, userID int) (map[string]any, error)"
|
||||||
|
description: "Obtiene los detalles de un usuario de Metabase por su ID numerico. Endpoint: GET /api/user/:id."
|
||||||
|
tags: [metabase, user, get, api]
|
||||||
|
uses_functions: []
|
||||||
|
uses_types: [MetabaseClient_go_infra]
|
||||||
|
returns: []
|
||||||
|
returns_optional: false
|
||||||
|
error_type: "error_go_core"
|
||||||
|
imports: [fmt]
|
||||||
|
tested: false
|
||||||
|
tests: []
|
||||||
|
test_file_path: ""
|
||||||
|
file_path: "functions/infra/metabase_get_user.go"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ejemplo
|
||||||
|
|
||||||
|
```go
|
||||||
|
user, err := MetabaseGetUser(client, 1)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
fmt.Println(user["email"], user["first_name"])
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notas
|
||||||
|
|
||||||
|
Retorna el objeto usuario completo como map. Error 404 si el ID no existe.
|
||||||
|
|
||||||
|
### Campos del usuario retornado
|
||||||
|
|
||||||
|
| Campo | Tipo | Descripcion |
|
||||||
|
|-------|------|-------------|
|
||||||
|
| id | float64 | ID numerico |
|
||||||
|
| email | string | Email |
|
||||||
|
| first_name | string | Nombre |
|
||||||
|
| last_name | string | Apellido |
|
||||||
|
| is_superuser | bool | Es admin |
|
||||||
|
| is_active | bool | Esta activo |
|
||||||
|
| common_name | string | Nombre completo |
|
||||||
|
| date_joined | string | Fecha de creacion |
|
||||||
|
| last_login | string | Ultimo login |
|
||||||
|
| group_ids | []float64 | IDs de grupos |
|
||||||
|
| locale | string | Locale del usuario |
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
package infra
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// metabaseRequest ejecuta una peticion HTTP contra la API de Metabase.
|
||||||
|
// method: GET, POST, PUT, DELETE
|
||||||
|
// baseURL: URL base sin trailing slash
|
||||||
|
// token: session token o API key
|
||||||
|
// path: ruta relativa (ej: "/api/user")
|
||||||
|
// body: payload JSON (nil para requests sin body)
|
||||||
|
// Retorna el body deserializado como map o nil si el body esta vacio.
|
||||||
|
func metabaseRequest(method, baseURL, token, path string, body map[string]any) (map[string]any, error) {
|
||||||
|
var reqBody io.Reader
|
||||||
|
if body != nil {
|
||||||
|
data, err := json.Marshal(body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("marshal body: %w", err)
|
||||||
|
}
|
||||||
|
reqBody = bytes.NewReader(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest(method, baseURL+path, reqBody)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("new request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("X-Metabase-Session", token)
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("http %s %s: %w", method, path, err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
respBody, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("read response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||||
|
return nil, fmt.Errorf("metabase %s %s: status %d: %s", method, path, resp.StatusCode, string(respBody))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(respBody) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var result map[string]any
|
||||||
|
if err := json.Unmarshal(respBody, &result); err != nil {
|
||||||
|
return nil, fmt.Errorf("unmarshal response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// metabaseRequestList es como metabaseRequest pero para endpoints que retornan un array JSON.
|
||||||
|
func metabaseRequestList(method, baseURL, token, path string, body map[string]any) ([]map[string]any, error) {
|
||||||
|
var reqBody io.Reader
|
||||||
|
if body != nil {
|
||||||
|
data, err := json.Marshal(body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("marshal body: %w", err)
|
||||||
|
}
|
||||||
|
reqBody = bytes.NewReader(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest(method, baseURL+path, reqBody)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("new request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("X-Metabase-Session", token)
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("http %s %s: %w", method, path, err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
respBody, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("read response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||||
|
return nil, fmt.Errorf("metabase %s %s: status %d: %s", method, path, resp.StatusCode, string(respBody))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(respBody) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var result []map[string]any
|
||||||
|
if err := json.Unmarshal(respBody, &result); err != nil {
|
||||||
|
return nil, fmt.Errorf("unmarshal response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package infra
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// MetabaseListCards lista preguntas/cards de Metabase.
|
||||||
|
// filter: "all", "mine", "fav", "archived", "recent", "popular", "database", "table" (vacio = todas).
|
||||||
|
// modelID: ID de database o tabla cuando filter es "database" o "table" (0 = ignorar).
|
||||||
|
func MetabaseListCards(client MetabaseClient, filter string, modelID int) ([]map[string]any, error) {
|
||||||
|
path := "/api/card"
|
||||||
|
sep := "?"
|
||||||
|
if filter != "" {
|
||||||
|
path += sep + "f=" + filter
|
||||||
|
sep = "&"
|
||||||
|
}
|
||||||
|
if modelID > 0 {
|
||||||
|
path += fmt.Sprintf("%smodel_id=%d", sep, modelID)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := metabaseRequestList("GET", client.BaseURL, client.Token, path, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("metabase list cards: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
---
|
||||||
|
name: metabase_list_cards
|
||||||
|
kind: function
|
||||||
|
lang: go
|
||||||
|
domain: infra
|
||||||
|
version: "1.0.0"
|
||||||
|
purity: impure
|
||||||
|
signature: "func MetabaseListCards(client MetabaseClient, filter string, modelID int) ([]map[string]any, error)"
|
||||||
|
description: "Lista preguntas/cards de Metabase con filtro opcional. Retorna array de cards. Endpoint: GET /api/card."
|
||||||
|
tags: [metabase, card, question, list, api]
|
||||||
|
uses_functions: []
|
||||||
|
uses_types: [MetabaseClient_go_infra]
|
||||||
|
returns: []
|
||||||
|
returns_optional: false
|
||||||
|
error_type: "error_go_core"
|
||||||
|
imports: [fmt]
|
||||||
|
tested: false
|
||||||
|
tests: []
|
||||||
|
test_file_path: ""
|
||||||
|
file_path: "functions/infra/metabase_list_cards.go"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ejemplo
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Listar todas las cards
|
||||||
|
cards, err := MetabaseListCards(client, "all", 0)
|
||||||
|
|
||||||
|
// Solo mis preguntas
|
||||||
|
cards, err := MetabaseListCards(client, "mine", 0)
|
||||||
|
|
||||||
|
// Cards de una database especifica
|
||||||
|
cards, err := MetabaseListCards(client, "database", 1)
|
||||||
|
|
||||||
|
// Cards archivadas
|
||||||
|
cards, err := MetabaseListCards(client, "archived", 0)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notas
|
||||||
|
|
||||||
|
### Parametros para un LLM
|
||||||
|
|
||||||
|
| Parametro | Tipo | Requerido | Descripcion |
|
||||||
|
|-----------|------|-----------|-------------|
|
||||||
|
| client | MetabaseClient | si | Cliente autenticado |
|
||||||
|
| filter | string | no | "all", "mine", "fav", "archived", "recent", "popular", "database", "table". Vacio = todas |
|
||||||
|
| modelID | int | no | ID de database/tabla. Solo aplica con filter "database" o "table". 0 = ignorar |
|
||||||
|
|
||||||
|
No tiene paginacion con offset/limit. Retorna todas las cards que coinciden.
|
||||||
|
|
||||||
|
### Campos principales de cada card
|
||||||
|
|
||||||
|
| Campo | Tipo | Descripcion |
|
||||||
|
|-------|------|-------------|
|
||||||
|
| id | float64 | ID numerico de la card |
|
||||||
|
| name | string | Nombre de la pregunta |
|
||||||
|
| description | string | Descripcion |
|
||||||
|
| display | string | Tipo de visualizacion (table, bar, line, pie, etc.) |
|
||||||
|
| collection_id | float64 | ID de la coleccion/carpeta |
|
||||||
|
| database_id | float64 | ID de la database |
|
||||||
|
| creator_id | float64 | ID del creador |
|
||||||
|
| archived | bool | Esta archivada |
|
||||||
|
| dataset_query | map | Query de la card (native o structured) |
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package infra
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// MetabaseListDashboards lista dashboards de Metabase.
|
||||||
|
// filter: "all", "mine" o "archived" (vacio = todas).
|
||||||
|
func MetabaseListDashboards(client MetabaseClient, filter string) ([]map[string]any, error) {
|
||||||
|
path := "/api/dashboard"
|
||||||
|
if filter != "" {
|
||||||
|
path += "?f=" + filter
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := metabaseRequestList("GET", client.BaseURL, client.Token, path, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("metabase list dashboards: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
---
|
||||||
|
name: metabase_list_dashboards
|
||||||
|
kind: function
|
||||||
|
lang: go
|
||||||
|
domain: infra
|
||||||
|
version: "1.0.0"
|
||||||
|
purity: impure
|
||||||
|
signature: "func MetabaseListDashboards(client MetabaseClient, filter string) ([]map[string]any, error)"
|
||||||
|
description: "Lista dashboards de Metabase con filtro opcional. Retorna array de dashboards resumidos (sin dashcards). Endpoint: GET /api/dashboard."
|
||||||
|
tags: [metabase, dashboard, list, api]
|
||||||
|
uses_functions: []
|
||||||
|
uses_types: [MetabaseClient_go_infra]
|
||||||
|
returns: []
|
||||||
|
returns_optional: false
|
||||||
|
error_type: "error_go_core"
|
||||||
|
imports: [fmt]
|
||||||
|
tested: false
|
||||||
|
tests: []
|
||||||
|
test_file_path: ""
|
||||||
|
file_path: "functions/infra/metabase_list_dashboards.go"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ejemplo
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Listar todos los dashboards
|
||||||
|
dashboards, err := MetabaseListDashboards(client, "all")
|
||||||
|
|
||||||
|
// Solo mis dashboards
|
||||||
|
dashboards, err := MetabaseListDashboards(client, "mine")
|
||||||
|
|
||||||
|
// Dashboards archivados
|
||||||
|
dashboards, err := MetabaseListDashboards(client, "archived")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notas
|
||||||
|
|
||||||
|
### Parametros para un LLM
|
||||||
|
|
||||||
|
| Parametro | Tipo | Requerido | Descripcion |
|
||||||
|
|-----------|------|-----------|-------------|
|
||||||
|
| client | MetabaseClient | si | Cliente autenticado |
|
||||||
|
| filter | string | no | "all", "mine", "archived". Vacio = todas |
|
||||||
|
|
||||||
|
Retorna dashboards resumidos (sin cards). Para ver las cards de un dashboard, usar MetabaseGetDashboard.
|
||||||
|
|
||||||
|
### Campos principales de cada dashboard
|
||||||
|
|
||||||
|
| Campo | Tipo | Descripcion |
|
||||||
|
|-------|------|-------------|
|
||||||
|
| id | float64 | ID del dashboard |
|
||||||
|
| name | string | Nombre |
|
||||||
|
| description | string | Descripcion |
|
||||||
|
| collection_id | float64 | Coleccion contenedora |
|
||||||
|
| creator_id | float64 | ID del creador |
|
||||||
|
| archived | bool | Archivado |
|
||||||
|
| created_at | string | Fecha creacion |
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package infra
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// MetabaseListUsers lista usuarios de Metabase con filtros opcionales.
|
||||||
|
// status: "active", "deactivated" o "all" (vacio = "active").
|
||||||
|
// query: filtro por nombre o email (vacio = sin filtro).
|
||||||
|
// limit/offset: paginacion (0 = valores por defecto de Metabase).
|
||||||
|
func MetabaseListUsers(client MetabaseClient, status, query string, limit, offset int) (map[string]any, error) {
|
||||||
|
path := "/api/user?"
|
||||||
|
if status != "" {
|
||||||
|
path += "status=" + status + "&"
|
||||||
|
}
|
||||||
|
if query != "" {
|
||||||
|
path += "query=" + query + "&"
|
||||||
|
}
|
||||||
|
if limit > 0 {
|
||||||
|
path += fmt.Sprintf("limit=%d&", limit)
|
||||||
|
}
|
||||||
|
if offset > 0 {
|
||||||
|
path += fmt.Sprintf("offset=%d&", offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := metabaseRequest("GET", client.BaseURL, client.Token, path, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("metabase list users: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
---
|
||||||
|
name: metabase_list_users
|
||||||
|
kind: function
|
||||||
|
lang: go
|
||||||
|
domain: infra
|
||||||
|
version: "1.0.0"
|
||||||
|
purity: impure
|
||||||
|
signature: "func MetabaseListUsers(client MetabaseClient, status, query string, limit, offset int) (map[string]any, error)"
|
||||||
|
description: "Lista usuarios de una instancia Metabase con filtros opcionales por estado, nombre/email y paginacion. Endpoint: GET /api/user. Requiere permisos de superusuario."
|
||||||
|
tags: [metabase, user, list, api]
|
||||||
|
uses_functions: []
|
||||||
|
uses_types: [MetabaseClient_go_infra]
|
||||||
|
returns: []
|
||||||
|
returns_optional: false
|
||||||
|
error_type: "error_go_core"
|
||||||
|
imports: [fmt]
|
||||||
|
tested: false
|
||||||
|
tests: []
|
||||||
|
test_file_path: ""
|
||||||
|
file_path: "functions/infra/metabase_list_users.go"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ejemplo
|
||||||
|
|
||||||
|
```go
|
||||||
|
client, _ := MetabaseAuth("http://localhost:3000", "admin@example.com", "pass")
|
||||||
|
|
||||||
|
// Listar todos los usuarios activos
|
||||||
|
users, err := MetabaseListUsers(client, "active", "", 0, 0)
|
||||||
|
|
||||||
|
// Buscar usuario por email
|
||||||
|
users, err := MetabaseListUsers(client, "", "john@", 10, 0)
|
||||||
|
|
||||||
|
// Listar desactivados
|
||||||
|
users, err := MetabaseListUsers(client, "deactivated", "", 25, 0)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notas
|
||||||
|
|
||||||
|
Retorna un map con la estructura paginada de Metabase:
|
||||||
|
- `data`: array de objetos usuario (id, email, first_name, last_name, is_superuser, etc.)
|
||||||
|
- `total`: numero total de usuarios que coinciden
|
||||||
|
- `limit`: tamanio de pagina usado
|
||||||
|
- `offset`: offset usado
|
||||||
|
|
||||||
|
### Parametros para un LLM
|
||||||
|
|
||||||
|
| Parametro | Tipo | Requerido | Descripcion |
|
||||||
|
|-----------|------|-----------|-------------|
|
||||||
|
| client | MetabaseClient | si | Cliente autenticado |
|
||||||
|
| status | string | no | "active" (default), "deactivated", "all" |
|
||||||
|
| query | string | no | Filtro por nombre o email |
|
||||||
|
| limit | int | no | Tamanio de pagina (0 = default Metabase) |
|
||||||
|
| offset | int | no | Offset para paginacion (0 = inicio) |
|
||||||
|
|
||||||
|
### Campos del usuario retornado
|
||||||
|
|
||||||
|
| Campo | Tipo | Descripcion |
|
||||||
|
|-------|------|-------------|
|
||||||
|
| id | float64 | ID numerico del usuario |
|
||||||
|
| email | string | Email unico |
|
||||||
|
| first_name | string | Nombre |
|
||||||
|
| last_name | string | Apellido |
|
||||||
|
| is_superuser | bool | Es admin |
|
||||||
|
| is_active | bool | Esta activo |
|
||||||
|
| common_name | string | Nombre completo |
|
||||||
|
| last_login | string | Fecha ultimo login |
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package infra
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// MetabaseUpdateCard actualiza campos de una card/pregunta en Metabase.
|
||||||
|
// fields es un map con los campos a actualizar.
|
||||||
|
// Campos comunes: name, description, display, dataset_query, visualization_settings,
|
||||||
|
// collection_id, archived, enable_embedding.
|
||||||
|
func MetabaseUpdateCard(client MetabaseClient, cardID int, fields map[string]any) (map[string]any, error) {
|
||||||
|
path := fmt.Sprintf("/api/card/%d", cardID)
|
||||||
|
|
||||||
|
result, err := metabaseRequest("PUT", client.BaseURL, client.Token, path, fields)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("metabase update card %d: %w", cardID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
---
|
||||||
|
name: metabase_update_card
|
||||||
|
kind: function
|
||||||
|
lang: go
|
||||||
|
domain: infra
|
||||||
|
version: "1.0.0"
|
||||||
|
purity: impure
|
||||||
|
signature: "func MetabaseUpdateCard(client MetabaseClient, cardID int, fields map[string]any) (map[string]any, error)"
|
||||||
|
description: "Actualiza campos de una card/pregunta en Metabase. Solo se modifican los campos incluidos en el map. Endpoint: PUT /api/card/:id."
|
||||||
|
tags: [metabase, card, question, update, api]
|
||||||
|
uses_functions: []
|
||||||
|
uses_types: [MetabaseClient_go_infra]
|
||||||
|
returns: []
|
||||||
|
returns_optional: false
|
||||||
|
error_type: "error_go_core"
|
||||||
|
imports: [fmt]
|
||||||
|
tested: false
|
||||||
|
tests: []
|
||||||
|
test_file_path: ""
|
||||||
|
file_path: "functions/infra/metabase_update_card.go"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ejemplo
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Cambiar nombre y descripcion
|
||||||
|
card, err := MetabaseUpdateCard(client, 42, map[string]any{
|
||||||
|
"name": "Updated Revenue Chart",
|
||||||
|
"description": "Now includes refunds",
|
||||||
|
})
|
||||||
|
|
||||||
|
// Archivar una card (soft-delete)
|
||||||
|
card, err := MetabaseUpdateCard(client, 42, map[string]any{
|
||||||
|
"archived": true,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Mover a otra coleccion
|
||||||
|
card, err := MetabaseUpdateCard(client, 42, map[string]any{
|
||||||
|
"collection_id": 10,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Cambiar la query SQL
|
||||||
|
card, err := MetabaseUpdateCard(client, 42, map[string]any{
|
||||||
|
"dataset_query": map[string]any{
|
||||||
|
"database": 1,
|
||||||
|
"type": "native",
|
||||||
|
"native": map[string]any{"query": "SELECT * FROM users LIMIT 100"},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notas
|
||||||
|
|
||||||
|
### Campos actualizables
|
||||||
|
|
||||||
|
| Campo | Tipo | Descripcion |
|
||||||
|
|-------|------|-------------|
|
||||||
|
| name | string | Nombre de la pregunta |
|
||||||
|
| description | string | Descripcion |
|
||||||
|
| display | string | Tipo de visualizacion |
|
||||||
|
| dataset_query | map | Query SQL o MBQL |
|
||||||
|
| visualization_settings | map | Config de visualizacion |
|
||||||
|
| collection_id | int | Mover a otra coleccion |
|
||||||
|
| archived | bool | Archivar/desarchivar (soft-delete) |
|
||||||
|
| enable_embedding | bool | Habilitar embedding publico |
|
||||||
|
| embedding_params | map | Parametros de embedding |
|
||||||
|
|
||||||
|
Solo incluir los campos que se quieren cambiar.
|
||||||
|
Para eliminar permanentemente usar MetabaseDeleteCard. Para soft-delete usar archived: true.
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package infra
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// MetabaseUpdateDashboard actualiza un dashboard en Metabase.
|
||||||
|
// fields puede incluir metadata del dashboard Y/O la lista completa de dashcards y tabs.
|
||||||
|
//
|
||||||
|
// Para gestionar cards en el dashboard, incluir "dashcards" en fields:
|
||||||
|
// - Agregar card: incluirla con ID negativo (ej: -1, -2)
|
||||||
|
// - Actualizar card: incluirla con su ID positivo existente
|
||||||
|
// - Eliminar card: omitirla del array (el array es el estado deseado completo)
|
||||||
|
//
|
||||||
|
// Campos comunes: name, description, archived, parameters, dashcards, tabs, collection_id.
|
||||||
|
func MetabaseUpdateDashboard(client MetabaseClient, dashboardID int, fields map[string]any) (map[string]any, error) {
|
||||||
|
path := fmt.Sprintf("/api/dashboard/%d", dashboardID)
|
||||||
|
|
||||||
|
result, err := metabaseRequest("PUT", client.BaseURL, client.Token, path, fields)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("metabase update dashboard %d: %w", dashboardID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
---
|
||||||
|
name: metabase_update_dashboard
|
||||||
|
kind: function
|
||||||
|
lang: go
|
||||||
|
domain: infra
|
||||||
|
version: "1.0.0"
|
||||||
|
purity: impure
|
||||||
|
signature: "func MetabaseUpdateDashboard(client MetabaseClient, dashboardID int, fields map[string]any) (map[string]any, error)"
|
||||||
|
description: "Actualiza un dashboard en Metabase incluyendo metadata, cards y tabs. El campo dashcards representa el estado completo deseado: cards nuevas con ID negativo, existentes con ID positivo, omitidas se eliminan. Endpoint: PUT /api/dashboard/:id."
|
||||||
|
tags: [metabase, dashboard, update, cards, api]
|
||||||
|
uses_functions: []
|
||||||
|
uses_types: [MetabaseClient_go_infra]
|
||||||
|
returns: []
|
||||||
|
returns_optional: false
|
||||||
|
error_type: "error_go_core"
|
||||||
|
imports: [fmt]
|
||||||
|
tested: false
|
||||||
|
tests: []
|
||||||
|
test_file_path: ""
|
||||||
|
file_path: "functions/infra/metabase_update_dashboard.go"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ejemplo
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Cambiar nombre
|
||||||
|
MetabaseUpdateDashboard(client, 1, map[string]any{
|
||||||
|
"name": "Updated Dashboard",
|
||||||
|
})
|
||||||
|
|
||||||
|
// Agregar una card al dashboard
|
||||||
|
// Primero obtener las dashcards existentes
|
||||||
|
dash, _ := MetabaseGetDashboard(client, 1)
|
||||||
|
existingCards := dash["dashcards"].([]any)
|
||||||
|
|
||||||
|
// Construir nuevo array con las existentes + la nueva
|
||||||
|
dashcards := make([]map[string]any, 0)
|
||||||
|
for _, dc := range existingCards {
|
||||||
|
dashcards = append(dashcards, dc.(map[string]any))
|
||||||
|
}
|
||||||
|
// Agregar nueva card (ID negativo = nueva)
|
||||||
|
dashcards = append(dashcards, map[string]any{
|
||||||
|
"id": -1, "card_id": 55, "size_x": 6, "size_y": 4, "col": 0, "row": 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
MetabaseUpdateDashboard(client, 1, map[string]any{
|
||||||
|
"dashcards": dashcards,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Archivar dashboard (soft-delete)
|
||||||
|
MetabaseUpdateDashboard(client, 1, map[string]any{"archived": true})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notas
|
||||||
|
|
||||||
|
### Gestion de dashcards (IMPORTANTE)
|
||||||
|
|
||||||
|
El array `dashcards` representa el **estado completo deseado** del dashboard:
|
||||||
|
|
||||||
|
| Accion | Como hacerlo |
|
||||||
|
|--------|-------------|
|
||||||
|
| Agregar card | Incluir con **ID negativo** (-1, -2, etc.) |
|
||||||
|
| Actualizar card | Incluir con su **ID positivo** existente |
|
||||||
|
| Eliminar card | **Omitir** del array |
|
||||||
|
| No cambiar cards | No incluir el campo dashcards |
|
||||||
|
|
||||||
|
**Flujo tipico para agregar una card:**
|
||||||
|
1. `MetabaseGetDashboard` para obtener dashcards existentes
|
||||||
|
2. Copiar las existentes al nuevo array
|
||||||
|
3. Agregar la nueva con ID negativo
|
||||||
|
4. Enviar el array completo
|
||||||
|
|
||||||
|
### Estructura de una dashcard
|
||||||
|
|
||||||
|
```go
|
||||||
|
map[string]any{
|
||||||
|
"id": -1, // negativo = nueva, positivo = existente
|
||||||
|
"card_id": 42, // ID de la card/pregunta
|
||||||
|
"size_x": 6, // ancho (1-18)
|
||||||
|
"size_y": 4, // alto
|
||||||
|
"col": 0, // columna (0-based)
|
||||||
|
"row": 0, // fila (0-based)
|
||||||
|
"dashboard_tab_id": nil, // tab (nil = sin tabs)
|
||||||
|
"parameter_mappings": []map[string]any{}, // mapeo de filtros
|
||||||
|
"visualization_settings": map[string]any{}, // settings custom
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Gestion de tabs
|
||||||
|
|
||||||
|
```go
|
||||||
|
map[string]any{
|
||||||
|
"tabs": []map[string]any{
|
||||||
|
{"id": 1, "name": "Overview"}, // tab existente
|
||||||
|
{"id": -1, "name": "Details"}, // tab nuevo (ID negativo)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Campos actualizables
|
||||||
|
|
||||||
|
| Campo | Tipo | Descripcion |
|
||||||
|
|-------|------|-------------|
|
||||||
|
| name | string | Nombre del dashboard |
|
||||||
|
| description | string | Descripcion |
|
||||||
|
| archived | bool | Archivar/desarchivar |
|
||||||
|
| dashcards | []map | Estado completo de cards |
|
||||||
|
| tabs | []map | Tabs del dashboard |
|
||||||
|
| parameters | []map | Filtros del dashboard |
|
||||||
|
| collection_id | int | Mover a otra coleccion |
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package infra
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// MetabaseUpdateUser actualiza campos de un usuario en Metabase.
|
||||||
|
// fields es un map con los campos a actualizar. Campos validos:
|
||||||
|
// first_name, last_name, email, is_superuser, group_ids, locale, login_attributes.
|
||||||
|
func MetabaseUpdateUser(client MetabaseClient, userID int, fields map[string]any) (map[string]any, error) {
|
||||||
|
path := fmt.Sprintf("/api/user/%d", userID)
|
||||||
|
|
||||||
|
result, err := metabaseRequest("PUT", client.BaseURL, client.Token, path, fields)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("metabase update user %d: %w", userID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
---
|
||||||
|
name: metabase_update_user
|
||||||
|
kind: function
|
||||||
|
lang: go
|
||||||
|
domain: infra
|
||||||
|
version: "1.0.0"
|
||||||
|
purity: impure
|
||||||
|
signature: "func MetabaseUpdateUser(client MetabaseClient, userID int, fields map[string]any) (map[string]any, error)"
|
||||||
|
description: "Actualiza campos de un usuario en Metabase. Solo se modifican los campos incluidos en el map. Requiere permisos de superusuario. Endpoint: PUT /api/user/:id."
|
||||||
|
tags: [metabase, user, update, api]
|
||||||
|
uses_functions: []
|
||||||
|
uses_types: [MetabaseClient_go_infra]
|
||||||
|
returns: []
|
||||||
|
returns_optional: false
|
||||||
|
error_type: "error_go_core"
|
||||||
|
imports: [fmt]
|
||||||
|
tested: false
|
||||||
|
tests: []
|
||||||
|
test_file_path: ""
|
||||||
|
file_path: "functions/infra/metabase_update_user.go"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ejemplo
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Cambiar nombre
|
||||||
|
user, err := MetabaseUpdateUser(client, 5, map[string]any{
|
||||||
|
"first_name": "Jane",
|
||||||
|
"last_name": "Smith",
|
||||||
|
})
|
||||||
|
|
||||||
|
// Promover a admin
|
||||||
|
user, err := MetabaseUpdateUser(client, 5, map[string]any{
|
||||||
|
"is_superuser": true,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Cambiar grupos
|
||||||
|
user, err := MetabaseUpdateUser(client, 5, map[string]any{
|
||||||
|
"group_ids": []int{1, 3, 5},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notas
|
||||||
|
|
||||||
|
### Campos actualizables
|
||||||
|
|
||||||
|
| Campo | Tipo | Descripcion |
|
||||||
|
|-------|------|-------------|
|
||||||
|
| first_name | string | Nombre |
|
||||||
|
| last_name | string | Apellido |
|
||||||
|
| email | string | Email (debe ser unico) |
|
||||||
|
| is_superuser | bool | Permisos de admin |
|
||||||
|
| group_ids | []int | IDs de grupos del usuario |
|
||||||
|
| locale | string | Locale (ej: "es", "en") |
|
||||||
|
| login_attributes | map | Atributos para sandboxing |
|
||||||
|
|
||||||
|
Solo incluir los campos que se quieren cambiar. Los demas se mantienen sin modificar.
|
||||||
|
Retorna el objeto usuario actualizado.
|
||||||
@@ -20,3 +20,9 @@ type ImageInfo struct {
|
|||||||
Size string
|
Size string
|
||||||
Created string
|
Created string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MetabaseClient holds the connection details for a Metabase instance API.
|
||||||
|
type MetabaseClient struct {
|
||||||
|
BaseURL string // e.g. "http://localhost:3000"
|
||||||
|
Token string // session token or API key
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package infra
|
||||||
|
|
||||||
|
// MetabaseClient holds the connection details for a Metabase instance API.
|
||||||
|
type MetabaseClient struct {
|
||||||
|
BaseURL string // e.g. "http://localhost:3000"
|
||||||
|
Token string // session token or API key
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
name: MetabaseClient
|
||||||
|
lang: go
|
||||||
|
domain: infra
|
||||||
|
version: "1.0.0"
|
||||||
|
algebraic: product
|
||||||
|
definition: |
|
||||||
|
type MetabaseClient struct {
|
||||||
|
BaseURL string
|
||||||
|
Token string
|
||||||
|
}
|
||||||
|
description: "Cliente para la API REST de Metabase. Contiene la URL base de la instancia y el token de autenticacion (session token o API key)."
|
||||||
|
tags: [metabase, api, client, infra]
|
||||||
|
uses_types: []
|
||||||
|
file_path: "types/infra/metabase_client.go"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notas
|
||||||
|
|
||||||
|
Tipo producto con dos campos obligatorios:
|
||||||
|
- `BaseURL`: URL base de la instancia Metabase sin trailing slash (ej: `http://localhost:3000`)
|
||||||
|
- `Token`: token de sesion obtenido con `MetabaseAuth()` o una API key creada en el admin UI de Metabase
|
||||||
|
|
||||||
|
El token se envia como header `X-Metabase-Session` en session tokens o `x-api-key` en API keys. Las funciones del registry usan `X-Metabase-Session` por defecto.
|
||||||
Reference in New Issue
Block a user