From 9e6bea681f8dcd01975eebf3cac7dde2c30b2b0d Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Sat, 28 Mar 2026 20:32:24 +0100 Subject: [PATCH] feat: funciones Go para API Metabase y tipo MetabaseClient MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- functions/infra/metabase_auth.go | 49 +++++++++ functions/infra/metabase_auth.md | 50 +++++++++ functions/infra/metabase_create_card.go | 35 ++++++ functions/infra/metabase_create_card.md | 86 +++++++++++++++ functions/infra/metabase_create_dashboard.go | 26 +++++ functions/infra/metabase_create_dashboard.md | 53 +++++++++ functions/infra/metabase_create_user.go | 28 +++++ functions/infra/metabase_create_user.md | 47 ++++++++ functions/infra/metabase_deactivate_user.go | 17 +++ functions/infra/metabase_deactivate_user.md | 40 +++++++ functions/infra/metabase_delete_card.go | 16 +++ functions/infra/metabase_delete_card.md | 37 +++++++ functions/infra/metabase_delete_dashboard.go | 16 +++ functions/infra/metabase_delete_dashboard.md | 37 +++++++ functions/infra/metabase_execute_card.go | 22 ++++ functions/infra/metabase_execute_card.md | 71 ++++++++++++ functions/infra/metabase_execute_query.go | 28 +++++ functions/infra/metabase_execute_query.md | 63 +++++++++++ functions/infra/metabase_get_card.go | 15 +++ functions/infra/metabase_get_card.md | 52 +++++++++ functions/infra/metabase_get_dashboard.go | 15 +++ functions/infra/metabase_get_dashboard.md | 71 ++++++++++++ functions/infra/metabase_get_user.go | 15 +++ functions/infra/metabase_get_user.md | 51 +++++++++ functions/infra/metabase_http.go | 107 ++++++++++++++++++ functions/infra/metabase_list_cards.go | 25 +++++ functions/infra/metabase_list_cards.md | 63 +++++++++++ functions/infra/metabase_list_dashboards.go | 19 ++++ functions/infra/metabase_list_dashboards.md | 57 ++++++++++ functions/infra/metabase_list_users.go | 30 +++++ functions/infra/metabase_list_users.md | 67 +++++++++++ functions/infra/metabase_update_card.go | 18 +++ functions/infra/metabase_update_card.md | 69 ++++++++++++ functions/infra/metabase_update_dashboard.go | 23 ++++ functions/infra/metabase_update_dashboard.md | 110 +++++++++++++++++++ functions/infra/metabase_update_user.go | 17 +++ functions/infra/metabase_update_user.md | 58 ++++++++++ functions/infra/types.go | 6 + types/infra/metabase_client.go | 7 ++ types/infra/metabase_client.md | 24 ++++ 40 files changed, 1640 insertions(+) create mode 100644 functions/infra/metabase_auth.go create mode 100644 functions/infra/metabase_auth.md create mode 100644 functions/infra/metabase_create_card.go create mode 100644 functions/infra/metabase_create_card.md create mode 100644 functions/infra/metabase_create_dashboard.go create mode 100644 functions/infra/metabase_create_dashboard.md create mode 100644 functions/infra/metabase_create_user.go create mode 100644 functions/infra/metabase_create_user.md create mode 100644 functions/infra/metabase_deactivate_user.go create mode 100644 functions/infra/metabase_deactivate_user.md create mode 100644 functions/infra/metabase_delete_card.go create mode 100644 functions/infra/metabase_delete_card.md create mode 100644 functions/infra/metabase_delete_dashboard.go create mode 100644 functions/infra/metabase_delete_dashboard.md create mode 100644 functions/infra/metabase_execute_card.go create mode 100644 functions/infra/metabase_execute_card.md create mode 100644 functions/infra/metabase_execute_query.go create mode 100644 functions/infra/metabase_execute_query.md create mode 100644 functions/infra/metabase_get_card.go create mode 100644 functions/infra/metabase_get_card.md create mode 100644 functions/infra/metabase_get_dashboard.go create mode 100644 functions/infra/metabase_get_dashboard.md create mode 100644 functions/infra/metabase_get_user.go create mode 100644 functions/infra/metabase_get_user.md create mode 100644 functions/infra/metabase_http.go create mode 100644 functions/infra/metabase_list_cards.go create mode 100644 functions/infra/metabase_list_cards.md create mode 100644 functions/infra/metabase_list_dashboards.go create mode 100644 functions/infra/metabase_list_dashboards.md create mode 100644 functions/infra/metabase_list_users.go create mode 100644 functions/infra/metabase_list_users.md create mode 100644 functions/infra/metabase_update_card.go create mode 100644 functions/infra/metabase_update_card.md create mode 100644 functions/infra/metabase_update_dashboard.go create mode 100644 functions/infra/metabase_update_dashboard.md create mode 100644 functions/infra/metabase_update_user.go create mode 100644 functions/infra/metabase_update_user.md create mode 100644 types/infra/metabase_client.go create mode 100644 types/infra/metabase_client.md diff --git a/functions/infra/metabase_auth.go b/functions/infra/metabase_auth.go new file mode 100644 index 00000000..38d62ecd --- /dev/null +++ b/functions/infra/metabase_auth.go @@ -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} +} diff --git a/functions/infra/metabase_auth.md b/functions/infra/metabase_auth.md new file mode 100644 index 00000000..8234de47 --- /dev/null +++ b/functions/infra/metabase_auth.md @@ -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 diff --git a/functions/infra/metabase_create_card.go b/functions/infra/metabase_create_card.go new file mode 100644 index 00000000..8ffe3818 --- /dev/null +++ b/functions/infra/metabase_create_card.go @@ -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 +} diff --git a/functions/infra/metabase_create_card.md b/functions/infra/metabase_create_card.md new file mode 100644 index 00000000..abeab982 --- /dev/null +++ b/functions/infra/metabase_create_card.md @@ -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": , + "type": "native", + "native": {"query": "SELECT ..."} +} +``` + +**MBQL (structured):** +```json +{ + "database": , + "type": "query", + "query": { + "source-table": , + "aggregation": [["count"]], + "breakout": [["field", , {"temporal-unit": "month"}]], + "filter": ["=", ["field", , null], "value"] + } +} +``` + +### Valores de display + +table, bar, line, pie, scalar, area, row, combo, funnel, map, scatter, waterfall, progress, gauge diff --git a/functions/infra/metabase_create_dashboard.go b/functions/infra/metabase_create_dashboard.go new file mode 100644 index 00000000..955100f9 --- /dev/null +++ b/functions/infra/metabase_create_dashboard.go @@ -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 +} diff --git a/functions/infra/metabase_create_dashboard.md b/functions/infra/metabase_create_dashboard.md new file mode 100644 index 00000000..120fb0e6 --- /dev/null +++ b/functions/infra/metabase_create_dashboard.md @@ -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. diff --git a/functions/infra/metabase_create_user.go b/functions/infra/metabase_create_user.go new file mode 100644 index 00000000..c72e7dea --- /dev/null +++ b/functions/infra/metabase_create_user.go @@ -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 +} diff --git a/functions/infra/metabase_create_user.md b/functions/infra/metabase_create_user.md new file mode 100644 index 00000000..21b62345 --- /dev/null +++ b/functions/infra/metabase_create_user.md @@ -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). diff --git a/functions/infra/metabase_deactivate_user.go b/functions/infra/metabase_deactivate_user.go new file mode 100644 index 00000000..df2cba9a --- /dev/null +++ b/functions/infra/metabase_deactivate_user.go @@ -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 +} diff --git a/functions/infra/metabase_deactivate_user.md b/functions/infra/metabase_deactivate_user.md new file mode 100644 index 00000000..dbb0d541 --- /dev/null +++ b/functions/infra/metabase_deactivate_user.md @@ -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. diff --git a/functions/infra/metabase_delete_card.go b/functions/infra/metabase_delete_card.go new file mode 100644 index 00000000..db4982ed --- /dev/null +++ b/functions/infra/metabase_delete_card.go @@ -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 +} diff --git a/functions/infra/metabase_delete_card.md b/functions/infra/metabase_delete_card.md new file mode 100644 index 00000000..d2c296f7 --- /dev/null +++ b/functions/infra/metabase_delete_card.md @@ -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. diff --git a/functions/infra/metabase_delete_dashboard.go b/functions/infra/metabase_delete_dashboard.go new file mode 100644 index 00000000..f12584a8 --- /dev/null +++ b/functions/infra/metabase_delete_dashboard.go @@ -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 +} diff --git a/functions/infra/metabase_delete_dashboard.md b/functions/infra/metabase_delete_dashboard.md new file mode 100644 index 00000000..a9e03fe8 --- /dev/null +++ b/functions/infra/metabase_delete_dashboard.md @@ -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})`. diff --git a/functions/infra/metabase_execute_card.go b/functions/infra/metabase_execute_card.go new file mode 100644 index 00000000..f7cda011 --- /dev/null +++ b/functions/infra/metabase_execute_card.go @@ -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 +} diff --git a/functions/infra/metabase_execute_card.md b/functions/infra/metabase_execute_card.md new file mode 100644 index 00000000..18aff315 --- /dev/null +++ b/functions/infra/metabase_execute_card.md @@ -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. diff --git a/functions/infra/metabase_execute_query.go b/functions/infra/metabase_execute_query.go new file mode 100644 index 00000000..5beed0ec --- /dev/null +++ b/functions/infra/metabase_execute_query.go @@ -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 +} diff --git a/functions/infra/metabase_execute_query.md b/functions/infra/metabase_execute_query.md new file mode 100644 index 00000000..a3bbe527 --- /dev/null +++ b/functions/infra/metabase_execute_query.md @@ -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" diff --git a/functions/infra/metabase_get_card.go b/functions/infra/metabase_get_card.go new file mode 100644 index 00000000..e68c3168 --- /dev/null +++ b/functions/infra/metabase_get_card.go @@ -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 +} diff --git a/functions/infra/metabase_get_card.md b/functions/infra/metabase_get_card.md new file mode 100644 index 00000000..57d3b5aa --- /dev/null +++ b/functions/infra/metabase_get_card.md @@ -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 | diff --git a/functions/infra/metabase_get_dashboard.go b/functions/infra/metabase_get_dashboard.go new file mode 100644 index 00000000..9bc7f3ce --- /dev/null +++ b/functions/infra/metabase_get_dashboard.go @@ -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 +} diff --git a/functions/infra/metabase_get_dashboard.md b/functions/infra/metabase_get_dashboard.md new file mode 100644 index 00000000..07147b91 --- /dev/null +++ b/functions/infra/metabase_get_dashboard.md @@ -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. diff --git a/functions/infra/metabase_get_user.go b/functions/infra/metabase_get_user.go new file mode 100644 index 00000000..308f47d9 --- /dev/null +++ b/functions/infra/metabase_get_user.go @@ -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 +} diff --git a/functions/infra/metabase_get_user.md b/functions/infra/metabase_get_user.md new file mode 100644 index 00000000..e884f25c --- /dev/null +++ b/functions/infra/metabase_get_user.md @@ -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 | diff --git a/functions/infra/metabase_http.go b/functions/infra/metabase_http.go new file mode 100644 index 00000000..d894bebe --- /dev/null +++ b/functions/infra/metabase_http.go @@ -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 +} diff --git a/functions/infra/metabase_list_cards.go b/functions/infra/metabase_list_cards.go new file mode 100644 index 00000000..ba1f5cd8 --- /dev/null +++ b/functions/infra/metabase_list_cards.go @@ -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 +} diff --git a/functions/infra/metabase_list_cards.md b/functions/infra/metabase_list_cards.md new file mode 100644 index 00000000..91656b15 --- /dev/null +++ b/functions/infra/metabase_list_cards.md @@ -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) | diff --git a/functions/infra/metabase_list_dashboards.go b/functions/infra/metabase_list_dashboards.go new file mode 100644 index 00000000..b541f39e --- /dev/null +++ b/functions/infra/metabase_list_dashboards.go @@ -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 +} diff --git a/functions/infra/metabase_list_dashboards.md b/functions/infra/metabase_list_dashboards.md new file mode 100644 index 00000000..4c588e51 --- /dev/null +++ b/functions/infra/metabase_list_dashboards.md @@ -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 | diff --git a/functions/infra/metabase_list_users.go b/functions/infra/metabase_list_users.go new file mode 100644 index 00000000..fe6971bd --- /dev/null +++ b/functions/infra/metabase_list_users.go @@ -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 +} diff --git a/functions/infra/metabase_list_users.md b/functions/infra/metabase_list_users.md new file mode 100644 index 00000000..764ef170 --- /dev/null +++ b/functions/infra/metabase_list_users.md @@ -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 | diff --git a/functions/infra/metabase_update_card.go b/functions/infra/metabase_update_card.go new file mode 100644 index 00000000..288bf8d0 --- /dev/null +++ b/functions/infra/metabase_update_card.go @@ -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 +} diff --git a/functions/infra/metabase_update_card.md b/functions/infra/metabase_update_card.md new file mode 100644 index 00000000..625d7e53 --- /dev/null +++ b/functions/infra/metabase_update_card.md @@ -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. diff --git a/functions/infra/metabase_update_dashboard.go b/functions/infra/metabase_update_dashboard.go new file mode 100644 index 00000000..79053b72 --- /dev/null +++ b/functions/infra/metabase_update_dashboard.go @@ -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 +} diff --git a/functions/infra/metabase_update_dashboard.md b/functions/infra/metabase_update_dashboard.md new file mode 100644 index 00000000..5e384d06 --- /dev/null +++ b/functions/infra/metabase_update_dashboard.md @@ -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 | diff --git a/functions/infra/metabase_update_user.go b/functions/infra/metabase_update_user.go new file mode 100644 index 00000000..5bd2677f --- /dev/null +++ b/functions/infra/metabase_update_user.go @@ -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 +} diff --git a/functions/infra/metabase_update_user.md b/functions/infra/metabase_update_user.md new file mode 100644 index 00000000..3976f233 --- /dev/null +++ b/functions/infra/metabase_update_user.md @@ -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. diff --git a/functions/infra/types.go b/functions/infra/types.go index 486f79b1..9d515777 100644 --- a/functions/infra/types.go +++ b/functions/infra/types.go @@ -20,3 +20,9 @@ type ImageInfo struct { Size 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 +} diff --git a/types/infra/metabase_client.go b/types/infra/metabase_client.go new file mode 100644 index 00000000..0bcbb4d0 --- /dev/null +++ b/types/infra/metabase_client.go @@ -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 +} diff --git a/types/infra/metabase_client.md b/types/infra/metabase_client.md new file mode 100644 index 00000000..be7b1765 --- /dev/null +++ b/types/infra/metabase_client.md @@ -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.