API REST de Lightdash 1.107.0 — referencia reversada

Endpoints usados para automatizar el setup inicial (usuario, organización, proyecto, token) y la creación de 3 dashboards con 17 gráficas por curl, sin pasar por la UI. Ninguno de estos aparece documentado a este nivel de detalle en docs.lightdash.com para esta versión — reversados leyendo directamente /usr/app/packages/backend/dist/generated/routes.js (rutas + schemas generados por tsoa) dentro del propio contenedor lightdash, y el bundle JS del frontend (/assets/*.js) cuando el nombre del método no bastaba para inferir el body exacto. Comando base usado para explorar:

docker exec lightdash grep -n '<patrón>' /usr/app/packages/backend/dist/generated/routes.js

Todos los ejemplos asumen sesión por cookie (-b cookies.txt -c cookies.txt) salvo donde se indica un Personal Access Token. Ver despliegue-cierre.md § Migración temporal de SECURE_COOKIES — con SECURE_COOKIES=true estos ejemplos solo funcionan contra https://dash.frento.com.mx, no contra http://localhost:8090.

Setup inicial (instancia sin organización todavía)

1. Registrar el primer usuario

POST /api/v1/user
{"firstName": "...", "lastName": "...", "email": "...", "password": "..."}

Devuelve organizationUuid: null — el usuario existe pero no pertenece a ninguna organización todavía. GET /api/v1/health confirma el estado con "requiresOrgRegistration": true.

2. Crear la organización — método PUT, no POST

PUT /api/v1/org
{"name": "Trivasa"}

Gotcha real: POST /api/v1/org devuelve 404 Not Found (la ruta simplemente no existe con ese verbo) — solo PUT está registrado (OperationId: CreateOrganization, decorador @Put() sobre la raíz del controller). El intento obvio (POST, por paralelismo con el registro de usuario) no funciona y el error 404 no da ninguna pista de que el verbo correcto es PUT.

Un endpoint que parecía candidato natural, PATCH /api/v1/user/me/complete (documentado en discusiones antiguas de GitHub del proyecto con el body {"organizationName": ..., "jobTitle": ..., ...}), no existe en esta versión — devuelve 403 Forbidden: "User is not part of an organization", señal de que esa ruta espera que la organización ya exista (probablemente vestigio de un flujo de onboarding distinto en versiones anteriores de Lightdash). No usar como referencia para 1.107.0.

POST /api/v1/login
{"email": "...", "password": "..."}

4. Personal Access Token (para el CLI)

POST /api/v1/user/me/personal-access-tokens
{"description": "cli-deploy", "expiresAt": null, "autoGenerated": false}

Respuesta incluye token en texto plano una sola vez (ldpat_...) — no se puede recuperar después, solo revocar/regenerar desde la UI (avatar → Personal access tokens).

Proyecto (warehouse + dbt)

POST /api/v1/org/projects
{
  "name": "Trivasa BI",
  "type": "DEFAULT",
  "dbtConnection": {"type": "none"},
  "dbtVersion": "latest",
  "warehouseConnection": {
    "type": "postgres",
    "host": "postgres-dw",
    "port": 5432,
    "user": "trivasa",
    "password": "...",
    "dbname": "trivasa_dw",
    "schema": "analytics",
    "sslmode": "disable"
  }
}

dbtConnection.type: "none" es el tipo correcto para el flujo CLI (el manifest se sube ya compilado desde fuera, el servidor nunca ejecuta dbt). Valores válidos del enum DbtProjectType: dbt (local, servidor ejecuta dbt), dbt_cloud_ide, github, gitlab, bitbucket, azure_devops, none, manifest.

sslmode: "disable" es obligatorio aquí porque postgres-dw no tiene SSL habilitado — sin este campo, cualquier compilación posterior falla con The server does not support SSL connections (mensaje textual de Postgres al rechazar la negociación SSL). No basta con ponerlo aquí — el CLI local también negocia SSL por su cuenta vía ~/.dbt/profiles.yml, que necesita su propio sslmode: disable en el output del profile (ver dbt-project.md o la página de wiki para el profiles.yml completo). Los dos son conexiones independientes al mismo Postgres; arreglar una sin la otra deja el error a mitad de camino.

No existe endpoint de actualización para un proyecto ya creado

Se buscó exhaustivamente (grep de todo app.patch/app.put sobre /api/v1/projects/:projectUuid sin subpath, y sobre /api/v1/org/projects) y no hay ninguna ruta que permita corregir warehouseConnection o dbtConnection de un proyecto existente en 1.107.0. Solo existe GET /api/v1/projects/:projectUuid. Un archivo del propio paquete @lightdash/cli (handlers/setWarehouse.js) apunta a PATCH /api/v1/projects/{projectUuid} como si existiera — no está registrado en routes.js de este build del servidor, ese comando del CLI está roto o depende de una versión de servidor distinta a la desplegada aquí. Tampoco se encontró endpoint de borrado de proyecto (DELETE) en ningún controller.

Consecuencia práctica: el primer proyecto creado sin sslmode quedó huérfano, sin explores, imposible de arreglar ni borrar por API — la única salida fue crear un segundo proyecto (POST /api/v1/org/projects de nuevo) con la config correcta desde cero. El proyecto viejo sigue existiendo en la organización, visible en la UI, 0 explores.

Space (contenedor requerido para dashboards)

POST /api/v1/projects/{projectUuid}/spaces
{"name": "Trivasa"}

Dashboard + charts en una sola llamada

POST /api/v1/projects/{projectUuid}/dashboards/with-charts
{
  "name": "Compras",
  "description": "...",
  "spaceUuid": "...",
  "charts": [ {...}, {...}, ... ]
}

Cada objeto de charts[]:

{
  "name": "Total comprado",
  "tableName": "fct_compras",
  "metricQuery": {
    "exploreName": "fct_compras",
    "dimensions": [],
    "metrics": ["fct_compras_total_compras"],
    "filters": {},
    "sorts": [],
    "limit": 1,
    "tableCalculations": []
  },
  "chartConfig": {
    "type": "big_number",
    "config": {"label": "Total comprado", "selectedField": "fct_compras_total_compras"}
  },
  "tableConfig": {"columnOrder": ["fct_compras_total_compras"]}
}

No incluir dashboardUuid ni spaceUuid dentro de cada chart — el schema CreateSavedChart es una unión (CreateChartInSpace | CreateChartInDashboard) donde el segundo variant pide dashboardUuid requerido, pero el endpoint bulk asigna los charts al dashboard recién creado automáticamente a partir del spaceUuid del body externo; incluir dashboardUuid: null a mano no hace falta y complica el payload sin necesidad. El layout de grid (posición x/y/w/h de cada tile) también se calcula solo — no hay forma de especificarlo en esta llamada, ni se intentó controlarlo.

Field ID = ${nombre_del_modelo_dbt}_${nombre_de_dimensión_o_métrica} — ej. fct_compras_total_compras, fct_movimientos_fecha_month. Se obtiene sin adivinar consultando GET /api/v1/projects/{projectUuid}/explores/{tableName} y leyendo las claves de results.tables[tableName].dimensions / .metrics.

chartConfig por tipo usado

typeconfig mínimo funcional
big_number{"label": "...", "selectedField": "<fieldId>"}
cartesian{"layout": {"xField": "<fieldId>", "yField": ["<fieldId>"], "flipAxes": true|false}, "eChartsConfig": {"series": [{"encode": {"xRef": {"field": "<fieldId>"}, "yRef": {"field": "<fieldId>"}}, "type": "bar"|"line"}]}}
pie{"groupFieldIds": ["<fieldId>"], "metricId": "<fieldId>", "showLegend": true, "showPercentage": true}
table{} (todos los campos son opcionales; tableConfig.columnOrder en el nivel superior del chart controla qué columnas muestra)

Todos los tipos de ChartConfig en el schema (BigNumberConfig, CartesianChartConfig, CustomVisConfig, PieChartConfig, FunnelChartConfig, TableChartConfig, TreemapChartConfig, GaugeChartConfig, DataAppVizChartConfig, MapChartConfig, SankeyChartConfig) usan la envoltura {"type": "<tipo>", "config": {...}} — solo se probaron los cuatro de la tabla de arriba.

sorts usa {"fieldId": "...", "descending": true|false} — útil para “top N” combinado con limit (ej. top 10 proveedores por gasto: sorts descendente por la métrica + limit: 10).

Explorar un modelo (para obtener sus field IDs)

GET /api/v1/projects/{projectUuid}/explores/{tableName}

Ejecutar la query de un chart guardado (verificar datos sin abrir la UI)

POST /api/v1/saved/{chartUuid}/results
{}

Útil para confirmar que un chart devuelve datos reales (o para reproducir un error de un chart roto) sin necesidad de cargar la UI completa — así se verificó cada bug de despliegue-cierre.md antes y después del fix.

Favoritos (sustituto FOSS del “pin to homepage”)

PATCH /api/v1/projects/{projectUuid}/favorites
{"contentUuid": "<uuid>", "contentType": "dashboard"}

contentType acepta chart | dashboard | space | data_app (enum ContentType). Alterna el estado (toggle) — la misma llamada quita el favorito si ya estaba puesto.

La pantalla “Welcome” con pinned items es Enterprise, no FOSS

GET /api/v1/projects/{projectUuid}/homepage devuelve:

{"status":"error","error":{"statusCode":422,"name":"MissingConfigError",
"message":"Unable to initialize service 'projectHomepageService' - no factory or provider."}}

El controller vive en packages/backend/dist/ee/controllers/projectHomepageController.js (carpeta ee/ = Enterprise Edition) con funciones de un “homepage builder” completo (getResolved, getForBuilder, viewAs por usuario/grupo/rol) — no es el mecanismo simple de “pin” de versiones antiguas de Lightdash. Sin license key (hasLicenseKey: false en /api/v1/health), el servicio nunca se inicializa. No hay forma de poblar esa pantalla en un deployment sin licencia, por API o por UI — Favoritos es el mecanismo real disponible.

Nota aparte: existe un PinningController (/api/v1/projects/{projectUuid}/pinned-lists/{pinnedListUuid}/items/order, GET/PATCH) y un método de servicio togglePinning en DashboardService, pero ningún controller expone togglePinning por REST en este build — parece código huérfano de un mecanismo de pin anterior al homepage EE actual, no usable desde afuera.

Véase también

  • despliegue-cierre.md — narrativa completa del despliegue (RAM/disco, los dos bugs post-deploy) donde se usó todo lo de este documento.
  • Lightdash — página de wiki con el resumen en prosa.