feat(metabase): expansion cards y documents — export, model, ProseMirror validation, copy nativo

Cards: export_card (CSV/XLSX/JSON), create_model (type=model para fuentes MBQL).
Documents: prosemirror_card_embed helper (resizeNode envolviendo cardEmbed),
validacion automatica contra whitelist TipTap antes de enviar, copy_document
refactorizado al endpoint nativo POST /api/document/:id/copy.
Docs: dataset_query legacy vs MBQL5, template-tags, whitelist de nodos.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-14 19:03:19 +02:00
parent cb392a48ee
commit cf70385bca
9 changed files with 516 additions and 71 deletions
@@ -3,17 +3,17 @@ name: metabase_create_document
kind: function
lang: py
domain: infra
version: "1.0.0"
version: "1.1.0"
purity: impure
signature: "def metabase_create_document(client: MetabaseClient, name: str, document: dict, collection_id: int = 0) -> dict"
description: "Crea un document nuevo con contenido ProseMirror. Endpoint: POST /api/document. Soporta cardEmbed, smartLink, flexContainer, callout, taskList y demas nodos custom de Metabase."
signature: "def metabase_create_document(client: MetabaseClient, name: str, document: dict, collection_id: int = 0, *, validate: bool = True) -> dict"
description: "Crea un document con contenido ProseMirror. Valida el arbol contra la whitelist de nodos ANTES de enviar (evita documentos que la API acepta pero el frontend renderiza vacíos). Usar prosemirror_card_embed() para embeber cards."
tags: [metabase, document, create, api, prosemirror, python]
uses_functions: []
uses_functions: [metabase_validate_document_payload_py_infra]
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: [httpx]
imports: [httpx, uuid]
params:
- name: client
desc: "instancia autenticada de MetabaseClient"
@@ -23,6 +23,8 @@ params:
desc: "arbol ProseMirror JSON: {type: 'doc', content: [...]}, o '' para arrancar vacio"
- name: collection_id
desc: "ID de coleccion destino (0 = root)"
- name: validate
desc: "si True (default), valida el ProseMirror antes de enviar. Lanza ValueError si hay nodos no soportados"
output: "dict: document recien creado con id, entity_id y metadata"
tested: false
tests: []
@@ -33,17 +35,49 @@ file_path: "python/functions/metabase/documents.py"
## Ejemplo
```python
doc = metabase_create_document(client, "Notas", {
from metabase.documents import metabase_create_document, prosemirror_card_embed
doc = metabase_create_document(client, "Reporte Q1", {
"type": "doc",
"content": [
{"type": "paragraph", "content": [{"type": "text", "text": "Hola"}]}
{"type": "heading", "attrs": {"level": 1},
"content": [{"type": "text", "text": "KPIs"}]},
{"type": "paragraph",
"content": [{"type": "text", "text": "Revenue por canal:"}]},
prosemirror_card_embed(42, height=450),
]
})
print(doc["id"])
```
## Nodos ProseMirror — whitelist
**Renderizan correctamente** (TipTap v0.59):
`doc, paragraph, text, heading, bulletList, orderedList, listItem, blockquote, codeBlock, horizontalRule, hardBreak, cardEmbed, flexContainer, smartLink, resizeNode, mention`
**La API acepta pero el frontend IGNORA** (resultado: documento vacío):
`callout, taskList, taskItem, details, table, tableRow, tableCell, image, iframe`
**Marks que renderizan:** `bold, italic, strike, code, link`
**Marks ignorados:** `underline, highlight, subscript, textStyle`
## cardEmbed — SIEMPRE envolver en resizeNode
Un `cardEmbed` desnudo renderiza pero queda con ~50px de alto. Metabase espera que vaya dentro de un `resizeNode`:
```python
# MAL — card diminuta
{"type": "cardEmbed", "attrs": {"id": 42}}
# BIEN — usar el helper
from metabase.documents import prosemirror_card_embed
prosemirror_card_embed(42, height=450)
# Genera: {"type": "resizeNode", "attrs": {"height": 450, "minHeight": 280},
# "content": [{"type": "cardEmbed", "attrs": {"id": 42, ...}}]}
```
## Notas
Nodos custom de Metabase observados (v0.59): `cardEmbed` (attrs.id=card_id), `smartLink` (attrs.entityId), `flexContainer` (attrs.columnWidths), `resizeNode`, `mention`. Marks estandar + `underline`, `highlight`, `subscript`, `textStyle`.
Cuando embebes un card via `cardEmbed`, Metabase crea una copia interna del card con `document_id` apuntando al document — no referencia el card original.
- La validación (`validate=True`) llama internamente a `metabase_validate_document_payload`. Si detecta nodos no soportados, lanza `ValueError` ANTES de hacer el POST — evita documentos que se ven vacíos.
- Pasar `validate=False` solo si se está experimentando con nodos nuevos.
- Para destacar texto, usar `blockquote` (NO `callout`).
- Cuando embebes un card via `cardEmbed`, Metabase crea una referencia al card — el card debe existir.