Fecha: 2026-06-17 → 2026-06-18 Estado: Aceptado Bloque: Server HTTP — catalog listing (Pushes 7, 10, 14 de la sesión 2026-06-17/18). Refina: ADR-0090 M13 (Sessions HTTP). ADR-0050 Z1 (USERS/ROLES). ADR-0025 Views. ADR-0052 Z3 RLS.

Contexto

Hasta esta sesión, el server gabysql-server exponía sólo 9 endpoints:

GET  /health   /metrics   /dbs   /tables   /schema   /rows
POST /exec     /tx/begin  /tx/commit  /tx/rollback

/tables listaba tablas + columnas + índices simples + FKs. El resto del catálogo vivía sólo dentro del proceso, accesible vía las API internas de Catalog:

// Disponibles en código, no en HTTP:
catalog.list_views()
catalog.list_policies()
catalog.list_triggers()
catalog.list_procedures()
catalog.list_functions()
catalog.list_users()
catalog.list_roles()
catalog.list_grants()

Eso forzaba a cualquier UI o cliente externo a hacer SQL especulativo (SELECT name FROM __views__, que ni siquiera existe como tabla exponible) o a parsear EXPLAIN/SHOW que tampoco listaban estos objetos. El admin web phpgabyadmin y el modelador gabymodeler no podían mostrar policies/triggers/procs/funcs/users/roles/grants — queda como caja negra.

Decisión post-Push 6: exponer los 8 list_* como GET endpoints.

Decisión

8 endpoints nuevos, todos GET, todos espejando 1:1 la API interna:

GET /views?db=<db>
GET /policies?db=<db>[&table=<t>]
GET /triggers?db=<db>[&table=<t>]
GET /procedures?db=<db>
GET /functions?db=<db>
GET /users?db=<db>
GET /roles?db=<db>
GET /grants?db=<db>[&grantee=<g>][&object=<o>]

Shape JSON

Cada respuesta envuelve un array en un objeto con ok: true:

GET /views    { "ok": true, "views":   [{ "name", "source", "columnAliases": [...] | null }] }
GET /roles    { "ok": true, "roles":   [{ "name" }] }
GET /grants   { "ok": true, "grants":  [{ "grantee", "object", "privs": ["SELECT","INSERT",...] }] }

Codes → keywords

Los enums internos (timing/event de triggers, action de policies, KDF scheme de users, bitmask de privs) se traducen a strings en el JSON. El cliente no debería conocer los shifts ni los códigos binarios — eso es detalle de persistencia.

Tipo Interno JSON
Trigger timing 0 / 1 "BEFORE" / "AFTER"
Trigger event 0 / 1 / 2 "INSERT" / "UPDATE" / "DELETE"
Policy action POLICY_ACTION_SELECT "SELECT""ALL"
User scheme 1 / 2 / 3 "pbkdf2-sha256" / "scrypt" / "argon2id"
Grant privs bitmask u32 ["SELECT", "INSERT", ...]

Seguridad — /users

Crítico: /users NO serializa password_hash ni salt. Sólo expone name, scheme (string legible) e iterations. Material secreto nunca debe filtrarse vía API HTTP, ni siquiera al cliente de gestión.

Esto se valida en tests/server_listing_endpoints.rs::users_endpoint_lists_user_without_secret_material con tres asserts negativos:

assert!(!body.contains("password_hash"));
assert!(!body.contains("\"salt\""));
assert!(!body.contains("hunter2"));        // el password en claro

Filtros

Tres endpoints aceptan filtros opcionales aplicados post-list (no via índice — el catálogo es chico):

Decisión: filtros post-list en vez de WHERE en el storage es tolerable porque el catálogo típico tiene <100 entradas total. Cuando un repo crezca a 10k policies tendrá que migrarse a un secondary index, pero esa es deuda futura, no de hoy.

Tests E2E

Push 15 agregó tests/server_listing_endpoints.rs con 10 tests:

views_endpoint_lists_declared_view
policies_endpoint_lists_declared_policies
policies_endpoint_filters_by_table
triggers_endpoint_lists_declared_trigger
procedures_endpoint_lists_declared_procedure
functions_endpoint_lists_declared_function
users_endpoint_lists_user_without_secret_material    ← seguridad
roles_endpoint_lists_declared_role
grants_endpoint_lists_privileges_as_keyword_array
grants_endpoint_filters_by_grantee

Total de tests del repo pasa de 828 a 838. CI verde en Ubuntu/macOS/ Windows + Docker.

Alternativas descartadas

Consecuencias

Positivas

Negativas / tradeoffs

Hotfix relacionado

El test del endpoint /users falló en CI inicial (Push 16 → CI rojo) porque mi mapeo de scheme en user_meta_json decía 1 => "argon2id" basado en una lectura errónea de docs/STATUS.md. El motor real devuelve scheme=2 (scrypt) como default. Pushes 16.fix1 + 15.fix2 corrigieron:

Lección lesson_docs_invariants_check re-aplicada: las afirmaciones sobre defaults del motor ahora se cross-checkean vs test E2E.

Referencias