feat: add issue 0008 — SQLite API Web service

App Go que expone registry.db y operations.db de cada app como API REST
read-only en localhost:8484, para acceso programático sin SQLite directo.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-08 01:27:54 +02:00
parent 4e8bbb0a88
commit 0bdb3d72d7
2 changed files with 184 additions and 0 deletions
+183
View File
@@ -0,0 +1,183 @@
# 0008 — SQLite API Web
## Metadata
| Campo | Valor |
|-------|-------|
| **ID** | 0008 |
| **Estado** | 🟡 pendiente |
| **Prioridad** | alta |
| **Tipo** | feature |
## Dependencias
Ninguna.
---
## Objetivo
App que expone `registry.db` y los `operations.db` de cada app como API REST HTTP, permitiendo que herramientas externas (dashboards, scripts, agentes, frontends) consulten las bases de datos del registry sin necesidad de acceso directo al filesystem ni SQLite CLI.
## Contexto
- Actualmente para consultar `registry.db` hay que estar en la misma máquina y usar `sqlite3` directamente o funciones Go que abren el archivo.
- Las apps existentes (metabase_registry, registry_dashboard) acceden a SQLite localmente. Cualquier herramienta nueva que necesite datos del registry tiene que reimplementar la conexión.
- Con una API web, cualquier cliente HTTP (curl, fetch, Python requests, frontends React) puede consultar el registry de forma uniforme.
- Metabase ya resuelve visualización, pero no da acceso programático limpio a los datos para agentes y scripts remotos.
## Arquitectura
```
apps/sqlite_api/
├── main.go — NEW: Entry point, configura rutas y arranca servidor
├── handlers.go — NEW: Handlers HTTP (query, tables, schema)
├── config.go — NEW: Configuración (puerto, DBs permitidas, read-only)
├── app.md — NEW: Metadata de la app (tag: service)
└── operations.db — Runtime: operaciones propias
```
### Patrón pure core / impure shell
- **Funciones del registry usadas:** `http_get_json_go_infra`, `http_post_json_go_infra` (para tests/clientes), `cache_to_sqlite_go_infra` (opcional para cache de queries)
- **Core puro:** validación de queries (solo SELECT/PRAGMA permitidos), parsing de parámetros, formateo de resultados JSON
- **Shell impuro:** servidor HTTP, apertura de SQLite, ejecución de queries
## Diseño de API
### Endpoints
```
GET /api/databases — Lista de DBs disponibles
GET /api/databases/:db/tables — Lista tablas de una DB
GET /api/databases/:db/schema — Schema completo (.schema)
POST /api/databases/:db/query — Ejecuta query SQL (solo SELECT)
GET /api/databases/:db/fts?q=texto&table=functions — Búsqueda FTS5 directa
GET /health — Health check
```
### Bases de datos expuestas
| Alias | Path real | Descripción |
|-------|-----------|-------------|
| `registry` | `registry.db` (raíz) | Funciones, tipos, proposals |
| `ops:{app}` | `apps/{app}/operations.db` | Entities, relations, executions de cada app |
### Seguridad
- **Read-only obligatorio:** Solo queries SELECT y PRAGMA. Cualquier INSERT/UPDATE/DELETE/DROP se rechaza antes de ejecutar.
- **Bind por defecto a localhost** (`127.0.0.1:8484`). Flag `--bind` para cambiar.
- **Sin autenticación** en v1 (solo acceso local). Documentar cómo poner detrás de reverse proxy si se necesita auth.
- **Query timeout:** máximo 5 segundos por query para evitar bloqueos.
- **Apertura con `?mode=ro`** en el connection string de SQLite para doble protección.
### Formato de respuesta
```json
// POST /api/databases/registry/query
// Body: {"sql": "SELECT id, name, purity FROM functions WHERE domain = 'core' LIMIT 5"}
{
"columns": ["id", "name", "purity"],
"rows": [
["filter_slice_go_core", "filter_slice", "pure"],
["map_slice_go_core", "map_slice", "pure"]
],
"count": 2,
"duration_ms": 3
}
```
## Tareas
### Fase 1: Servidor base
- [ ] **1.1** Crear `apps/sqlite_api/` con `main.go`, `go.mod` (o usar módulo raíz)
- [ ] **1.2** Handler `/health` y `/api/databases` (lista estática de DBs detectadas)
- [ ] **1.3** Handler `POST /api/databases/:db/query` con validación read-only
- [ ] **1.4** Abrir DBs con `?mode=ro` y `-tags fts5`
- [ ] **1.5** `app.md` con tag `service`, documentar puerto y health check
### Fase 2: Endpoints de exploración
- [ ] **2.1** Handler `/api/databases/:db/tables` (lista tablas vía `sqlite_master`)
- [ ] **2.2** Handler `/api/databases/:db/schema` (output de `.schema`)
- [ ] **2.3** Handler `/api/databases/:db/fts` para búsqueda FTS5 sin escribir SQL
### Fase 3: Operations discovery
- [ ] **3.1** Auto-detectar `apps/*/operations.db` al arrancar
- [ ] **3.2** Exponer cada operations.db como `ops:{app_name}`
- [ ] **3.3** Endpoint `GET /api/databases` incluye las operations detectadas
### Fase 4: Cleanup y docs
- [ ] Crear `app.md` completo
- [ ] Ejecutar `go vet` y `go test`
- [ ] Actualizar issue en `dev/issues/README.md`
---
## Ejemplo de uso
```bash
# Arrancar el servicio
cd apps/sqlite_api && go run . --port 8484
# Health check
curl http://localhost:8484/health
# Listar databases disponibles
curl http://localhost:8484/api/databases
# Query al registry
curl -X POST http://localhost:8484/api/databases/registry/query \
-H "Content-Type: application/json" \
-d '{"sql": "SELECT id, purity, description FROM functions WHERE domain = '\''core'\'' LIMIT 5"}'
# Búsqueda FTS5
curl "http://localhost:8484/api/databases/registry/fts?q=slice&table=functions"
# Schema
curl http://localhost:8484/api/databases/registry/schema
# Query a operations de una app
curl -X POST http://localhost:8484/api/databases/ops:pipeline_launcher/query \
-H "Content-Type: application/json" \
-d '{"sql": "SELECT * FROM executions ORDER BY started_at DESC LIMIT 10"}'
```
```python
# Desde Python
import requests
r = requests.post("http://localhost:8484/api/databases/registry/query", json={
"sql": "SELECT id, name FROM functions WHERE purity = 'pure' AND domain = 'core'"
})
data = r.json()
for row in data["rows"]:
print(row[0], row[1])
```
## Decisiones de diseño
- **Go con net/http estándar**: sin framework externo, coherente con el resto del registry. Router simple con `http.ServeMux`.
- **Puerto 8484**: no colisiona con Metabase (3000), Jupyter (8888), ni otros servicios comunes.
- **Read-only estricto**: la API nunca modifica datos. Para escribir se usan los mecanismos existentes (`fn ops`, `fn index`).
- **Sin ORM**: queries se pasan tal cual a SQLite. El valor es el acceso HTTP, no una capa de abstracción SQL.
- **Auto-discovery de operations.db**: escanea `apps/*/operations.db` al inicio para no tener que configurar cada app manualmente.
## Riesgos
- **SQL injection vía queries arbitrarias**: Mitigado con apertura read-only (`?mode=ro`) + validación de que el statement empieza con SELECT o PRAGMA.
- **Queries pesadas bloquean el servidor**: Mitigado con timeout de 5s por query y context cancelable.
- **Archivos SQLite bloqueados por escritores concurrentes**: Mitigado con `journal_mode=wal` y apertura read-only que no bloquea escritores.
## Criterios de aceptación
- [ ] `curl localhost:8484/health` retorna 200
- [ ] Queries SELECT funcionan contra registry.db
- [ ] Queries INSERT/UPDATE/DELETE son rechazadas con 400
- [ ] Operations.db de apps existentes son accesibles como `ops:{nombre}`
- [ ] FTS5 funciona a través de la API
- [ ] Tag `service` en app.md
- [ ] El servidor arranca con `go run .` sin configuración adicional
+1
View File
@@ -13,3 +13,4 @@
| **0007c** | **DAG engine: execution store (SQLite)** | pendiente | alta | feature | 0007e |
| **0007d** | **DAG engine: scheduler (cron parser, ticker)** | pendiente | media | feature | 0007e |
| **0007e** | **DAG engine: app CLI que reemplaza Dagu** | pendiente | alta | feature | — |
| **0008** | **SQLite API Web** | pendiente | alta | feature | — |