API de Managed Tables
Endpoints para gestionar tablas PostgreSQL propias de la company. Cada company tiene un banco managed dedicado, provisionado on-demand. Soporta creacion manual de tablas, upload de archivos (CSV/Excel/ZIP) con inferencia de schema, y CRUD de filas.
Diferencia con respecto a Data API:
- Data API = consumo de visualizaciones guardadas como endpoint publico (read-only via API key).
- Managed Tables = capa de almacenamiento del usuario (CRUD via JWT del console).
Las visualizaciones publicadas via Data API pueden leer de tablas managed-tables, pero los dos modulos son independientes.
Todos los endpoints requieren autenticacion JWT estandar (Authorization: Bearer <token>) y estan bajo el prefijo /v1/managed-tables.
Provisioning
Provisionar banco managed
/v1/managed-tables/provisionCrea un banco PostgreSQL dedicado para la company (idempotente — re-llamar devuelve la conexion existente).
Permiso requerido: managed-tables:create
Respuesta: 200 OK
{
"provisioned": true,
"connection_id": "507f1f77bcf86cd799439011"
}
Estado del provisionamiento
/v1/managed-tables/statusPermiso requerido: managed-tables:list
Respuesta: 200 OK
{
"provisioned": true,
"connection_id": "507f1f77bcf86cd799439011"
}
provisioned: false indica que POST /provision aun no se ejecuto.
CRUD de Tablas
Listar tablas
/v1/managed-tablesPermiso requerido: managed-tables:list
Query Parameters:
| Parametro | Tipo | Default | Descripcion |
|---|---|---|---|
page | integer | 1 | Numero de pagina |
per_page | integer | 20 | Items por pagina (max 100) |
search | string | - | Filtra por display_name (case-insensitive) |
Respuesta: 200 OK
{
"total": 12,
"quantity": 12,
"records": [
{
"_id": "507f1f77bcf86cd799439011",
"company_id": "507f1f77bcf86cd799439012",
"connection_id": "507f1f77bcf86cd799439013",
"display_name": "vendas_mensais",
"columns": [
{ "name": "produto", "display_name": "Producto", "type": "text", "nullable": false },
{ "name": "valor", "display_name": "Valor", "type": "decimal", "nullable": true }
],
"source": "manual",
"status": "active",
"row_count": 1500,
"created_by_user_id": "507f1f77bcf86cd799439014",
"created_at": "2026-01-15T10:30:00.000Z",
"updated_at": "2026-01-15T10:30:00.000Z"
}
]
}
Obtener tabla
/v1/managed-tables/:idPermiso requerido: managed-tables:list
Respuesta: 200 OK — mismo shape de un item de records del listado.
Errores: 404 { "error": "Tabela não encontrada" }
Crear tabla (manual)
/v1/managed-tablesPermiso requerido: managed-tables:create
Pre-requisito: banco provisionado (POST /provision). De lo contrario devuelve 400 Base de dados não provisionada. Ative "Meus Dados" primeiro.
Body:
| Campo | Tipo | Obligatorio | Descripcion |
|---|---|---|---|
display_name | string | Si | Nombre de la tabla. Debe coincidir con ^[a-z_][a-z0-9_]*$ (snake_case sin empezar con numero). |
columns | array | Si | Lista de columnas (>=1). |
columns[].name | string | Si | Nombre PG (snake_case). |
columns[].display_name | string | Si | Nombre amigable. |
columns[].type | enum | Si | text | integer | decimal | boolean | date | datetime. |
columns[].nullable | boolean | Si | Si acepta NULL. |
{
"display_name": "vendas_mensais",
"columns": [
{ "name": "produto", "display_name": "Producto", "type": "text", "nullable": false },
{ "name": "valor", "display_name": "Valor", "type": "decimal", "nullable": true }
]
}
Respuesta: 201 Created — mismo shape que GET /:id.
Errores:
| HTTP | Codigo | Descripcion |
|---|---|---|
| 400 | MANAGED_TABLE_DUPLICATE_NAME | Nombre ya existe activo |
| 400 | - | Banco no provisionado |
| 422 | - | Schema invalido (display_name o columns) |
Actualizar tabla
/v1/managed-tables/:idPermiso requerido: managed-tables:update
Body (campos opcionales):
{
"display_name": "novo_nome",
"columns": [...],
"status": "active"
}
Respuesta: 200 OK
Eliminar tabla
/v1/managed-tables/:idPermiso requerido: managed-tables:delete
Soft-delete: la tabla desaparece de los listados. Restauracion no es expuesta por la API.
Respuesta: 204 No Content
Upload de archivos
Flujo de 3 etapas:
- Upload → crea job en estado
queued. Worker procesa el archivo e infiere schema (estadoanalisando→waiting_confirm). - Confirm → usuario revisa el schema inferido y confirma; el job pasa a
inserting→done. - Polling → frontend monitorea el estado via
GET /upload-jobs/:jobId.
Acepta .csv, .xlsx, .xls, .zip. Hasta 10 archivos por upload, 100MB total.
Upload (crear tabla nueva)
/v1/managed-tables/uploadPermiso requerido: managed-tables:upload
Content-Type: multipart/form-data
Form fields:
| Campo | Tipo | Descripcion |
|---|---|---|
files | file[] | 1-10 archivos (campo repetido). |
Respuesta: 201 Created
{
"job_id": "507f1f77bcf86cd799439011",
"status": "queued"
}
Upload (anexar a tabla existente)
/v1/managed-tables/:id/uploadPermiso requerido: managed-tables:upload
Content-Type: multipart/form-data
Form fields:
| Campo | Tipo | Default | Descripcion |
|---|---|---|---|
files | file[] | - | 1-10 archivos. |
mode | string | append | append (agrega filas) o replace (truncate + insert). |
Respuesta: mismo shape del upload de tabla nueva.
Listar jobs activos
/v1/managed-tables/upload-jobs/activePermiso requerido: managed-tables:list
Devuelve jobs aun no terminales (queued, analisando, waiting_confirm, inserting).
Respuesta: 200 OK
{
"records": [
{
"_id": "...",
"company_id": "...",
"user_table_id": null,
"files": [
{ "original_name": "vendas.csv", "storage_path": "...", "size_bytes": 12345, "mime_type": "text/csv" }
],
"mode": "create",
"status": "waiting_confirm",
"inferred_schema": {
"suggested_table_name": "vendas",
"columns": [{ "name": "produto", "display_name": "Producto", "type": "text", "nullable": false }],
"sample_rows": [{ "produto": "X" }],
"total_row_count_estimate": 1500
},
"confirmed_schema": null,
"progress": null,
"error": null,
"created_at": "2026-01-15T10:30:00.000Z",
"updated_at": "2026-01-15T10:30:00.000Z"
}
]
}
Obtener job
/v1/managed-tables/upload-jobs/:jobIdPermiso requerido: managed-tables:list
Respuesta: 200 OK — mismo shape del item de active.
Estados posibles: queued, analisando, waiting_confirm, inserting, done, failed.
Errores: 404 { "error": "Job não encontrado" }
Confirmar schema del job
/v1/managed-tables/upload-jobs/:jobId/confirmPermiso requerido: managed-tables:upload
Aplica el schema confirmado por el usuario y enqueua la fase inserting.
Body:
{
"display_name": "vendas",
"columns": [
{ "name": "produto", "display_name": "Producto", "type": "text", "nullable": false }
]
}
Para upload en tabla existente (user_table_id seteado en el job), el display_name reusa el nombre legacy y no se valida contra el regex.
Respuesta: 200 OK
{ "status": "inserting" }
Errores:
| HTTP | Codigo | Descripcion |
|---|---|---|
| 400 | - | Job no esta en waiting_confirm |
| 400 | MANAGED_TABLE_DUPLICATE_NAME | Nombre ya existe (solo en creacion) |
| 422 | - | display_name invalido (regex), columna invalida (name/type) |
CRUD de Datos
Listar filas
/v1/managed-tables/:id/dataPermiso requerido: managed-tables:data-read
Query Parameters:
| Parametro | Tipo | Default | Descripcion |
|---|---|---|---|
page | integer | 1 | Numero de pagina |
per_page | integer | 50 | Items por pagina |
sort_by | string | - | field ASC o -field DESC |
Respuesta: 200 OK
{
"total": 1500,
"quantity": 50,
"records": [
{ "id": "uuid", "produto": "X", "valor": 12.5 }
]
}
Insertar filas
/v1/managed-tables/:id/dataPermiso requerido: managed-tables:data-write
Body:
{
"rows": [
{ "produto": "X", "valor": 12.5 },
{ "produto": "Y", "valor": 30 }
]
}
Respuesta: 201 Created
{ "inserted": 2 }
Errores: 422 si rows ausente o vacio.
Actualizar fila
/v1/managed-tables/:id/data/:rowIdPermiso requerido: managed-tables:data-write
Body: objeto con los campos a actualizar.
Respuesta: 200 OK — fila actualizada.
Errores: 404 { "error": "Linha não encontrada" }
Eliminar fila
/v1/managed-tables/:id/data/:rowIdPermiso requerido: managed-tables:data-write
Respuesta: 204 No Content
Errores: 404 { "error": "Linha não encontrada" }
Tipos de columna
| Tipo | PostgreSQL DDL | Descripcion |
|---|---|---|
text | TEXT | String libre |
integer | BIGINT | Entero 64-bit |
decimal | NUMERIC | Numerico de precision arbitraria |
boolean | BOOLEAN | true/false |
date | DATE | Fecha (sin hora) |
datetime | TIMESTAMPTZ | Timestamp con timezone |
Limites y restricciones
- Upload: max 100MB total, max 10 archivos por llamada.
- Nombre de tabla / columna: regex
^[a-z_][a-z0-9_]*$. display_namede la tabla es unico por company entre tablas activas.DELETEes soft-delete: la tabla desaparece de los listados. Restauracion no es expuesta por la API.
Codigos de error
| HTTP | Codigo | Descripcion |
|---|---|---|
| 400 | MANAGED_TABLE_DUPLICATE_NAME | Conflicto de display_name en la company |
| 400 | - | Banco no provisionado / job no esta en waiting_confirm |
| 401 | - | JWT ausente o invalido |
| 403 | - | Sin permiso IAM (managed-tables:*) |
| 404 | - | Tabla, fila o job inexistente |
| 422 | - | Schema invalido (regex de identificador / tipo de columna) |
| 500 | - | Error interno |