diff --git a/.claude/commands/meta_bigq.md b/.claude/commands/meta_bigq.md index ed92cec9..189b70ef 100644 --- a/.claude/commands/meta_bigq.md +++ b/.claude/commands/meta_bigq.md @@ -109,6 +109,125 @@ metabase_update_dashboard(client, dash["id"], dashcards=[ **Filtros de list_dashboards:** `all`, `mine`, `archived` +### Documents (ProseMirror) + +Los "documents" son páginas narrativas editables con texto rico y cards embebidas. **No hay helpers en fn_registry todavía** — usa el endpoint REST directamente a través de `client._http`. + +**Endpoints:** + +| Método | Ruta | Qué hace | +|--------|------|---------| +| GET | `/api/document` | Lista documents (`{items: [...]}`) | +| GET | `/api/document/{id}` | Lee un document (incluye `document` con árbol ProseMirror) | +| POST | `/api/document` | Crea. Payload: `{name, collection_id, document}` | +| PUT | `/api/document/{id}` | Actualiza. Mismo payload que POST | +| PUT | `/api/document/{id}` con `{archived: true}` | Soft-delete | + +```python +# Crear documento +resp = client._http.request("POST", "/api/document", json={ + "name": "Mi análisis", + "collection_id": 583, # obligatorio — raíz no se acepta desde API + "document": {"type": "doc", "content": [ + {"type": "heading", "attrs": {"level": 1}, "content": [{"type": "text", "text": "Título"}]}, + {"type": "paragraph", "content": [{"type": "text", "text": "Cuerpo."}]}, + ]}, +}) +doc_id = resp.json()["id"] +print(f"https://reports.autingo.es/document/{doc_id}") +``` + +#### Tipos de nodo SOPORTADOS en Metabase v0.59.x + +Solo estos tipos renderizan. **Cualquier tipo fuera de esta lista hace que el documento se vea vacío al abrirlo.** + +```python +ALLOWED_DOC_NODES = { + "doc", "heading", "paragraph", "text", + "horizontalRule", "blockquote", + "bulletList", "listItem", + "codeBlock", # attrs.language ej: "sql" + "resizeNode", # envuelve SIEMPRE a cardEmbed + "cardEmbed", # solo dentro de resizeNode +} +``` + +Marcas inline válidas en nodos `text`: `bold`, `italic`, `code`, `strike` (se aplican con `"marks": [{"type": "bold"}, ...]`). + +#### Tipos PROHIBIDOS (rompen el render) + +- `table`, `tableRow`, `tableHeader`, `tableCell` → en v0.59.x no están registrados en el schema del editor y el doc entero se vuelve invisible. +- `callout` → idem (documentado en memoria `feedback_metabase_prosemirror.md`). +- `image`, `video`, `iframe`, `mention`, cualquier embed de terceros → no registrados. + +Si necesitas una tabla, **emúlala con una `bulletList` de `**clave:** valor`**: + +```python +def kv_list(pairs): + return {"type": "bulletList", "content": [ + {"type": "listItem", "content": [ + {"type": "paragraph", "content": [ + {"type": "text", "text": k, "marks": [{"type": "bold"}]}, + {"type": "text", "text": f": {v}"}, + ]}, + ]} + for k, v in pairs + ]} +``` + +#### cardEmbed SIEMPRE dentro de resizeNode + +Un `cardEmbed` suelto no renderiza. Patrón obligatorio: + +```python +def card_embed(card_id, height=420): + import uuid + return { + "type": "resizeNode", + "attrs": {"height": height, "minHeight": 280}, + "content": [{ + "type": "cardEmbed", + "attrs": {"id": card_id, "name": None, "_id": str(uuid.uuid4())}, + }], + } +``` + +#### Validación OBLIGATORIA antes de POST/PUT + +Nunca envíes un document a Metabase sin validar primero. Un solo nodo prohibido lo deja invisible sin devolver error HTTP: + +```python +ALLOWED = {"doc","heading","paragraph","text","horizontalRule","blockquote", + "bulletList","listItem","codeBlock","resizeNode","cardEmbed"} + +def validate_doc(node, path=""): + errs = [] + if isinstance(node, dict): + typ = node.get("type", "?") + if typ not in ALLOWED: + errs.append(f"{path}: tipo no permitido '{typ}'") + if typ == "resizeNode": + inner = node.get("content", []) + if not (len(inner) == 1 and inner[0].get("type") == "cardEmbed"): + errs.append(f"{path}: resizeNode debe contener exactamente un cardEmbed") + return errs # no re-descender al cardEmbed interno + for i, c in enumerate(node.get("content", []) or []): + errs += validate_doc(c, f"{path}/{typ}[{i}]") + return errs + +errs = validate_doc(my_doc) +assert not errs, f"Doc inválido:\n" + "\n".join(f" - {e}" for e in errs) +``` + +#### Aprender estructura de un doc que ya funciona + +Si dudas sobre un nodo, **clónalo de un doc existente que renderice**: + +```python +d = client._http.request("GET", "/api/document/2").json() +# d["document"] contiene el árbol completo en ProseMirror +``` + ### Databases ```python