Arquitectura
GM Hades Backend implementa Clean Architecture en tres capas que no se importan entre sí hacia arriba. Ningún componente de una capa interna conoce ni importa código de una capa externa. Esta restricción se verifica manualmente con grep sobre los imports antes de cada merge a main.
Las tres capas
graph TD
C["Handler Layer\nValidación de input, marshaling gRPC,\nsanitización de errores"]
S["Service Layer\nLógica de negocio, orquestación,\npropagación de timeouts"]
R["Repository Layer\nEjecución SQL, túneles SSH,\nSFTP, caché de credenciales"]
E["External Systems\nSQL Server, MariaDB vía SSH,\nSFTP server"]
C --> S --> R --> E
Handler Layer. Recibe la petición gRPC, valida el input (formato RFC, límites de paginación, tipo de consulta), registra el query activo con su cancelFunc, y transforma los modelos de dominio en respuestas proto. Es la única capa que importa los paquetes proto/ generados.
Service Layer. Orquesta la operación: obtiene la configuración del tenant, construye el contexto con timeout, llama a los repositorios y combina los resultados. No importa database/sql, golang.org/x/crypto/ssh ni ningún driver. Solo conoce interfaces.
Repository Layer. Implementa el acceso a los sistemas externos: conexión a SQL Server, caché de credenciales en proceso, túnel SSH + MySQL para MariaDB, cliente SFTP. Es la única capa que importa drivers de base de datos y librerías de red.
Estructura de directorios
cmd/ server/main.go Punto de entrada: DI, registro de servicios, cmux
internal/ config/config.go AppConfig cargado desde variables de entorno handlers/ query.go Handler v1 query_v2.go Handler v2: ExecuteQuery, CancelQuery, GetActiveQueries query_v2_batch.go Handler v2: StreamQuery, ExecuteBatch, ExecuteTransaction query_v2_schema.go Handler v2: GetTableSchema, ListTables, ValidateQuery health.go grpc.health.v1.Health services/ query_execution.go Orquestación: credenciales + conexión + ejecución repository/ credentials.go Credenciales desde ADMON SQL Server + caché 15 min database.go Conexión SQL Server, ejecución, paginación, búsqueda, sort
boveda_fiscal/ config/loader.go Carga connections.json; resuelve passwords desde env handlers/ db_handler.go BovedaFiscalDBService handler sftp_handler.go BovedaFiscalSFTPService handler services/ db_service.go Orquestación de consultas MariaDB sftp_service.go Orquestación de operaciones SFTP repository/ mariadb.go Túnel SSH + ejecución MariaDB, streaming, esquema sftp.go Cliente SFTP, listado, descarga, búsqueda UUID, ZIP
pkg/ logger/logger.go Interfaz Logger + implementación Zap utils/ validation.go IsValidRFC, IsValidQuery, SanitizeSQL errors.go TimeoutError, ValidationError, DatabaseError, etc.
proto/ query.proto v1 query_v2.proto v2 (778 líneas, completamente documentado) boveda_fiscal_db.proto Bóveda Fiscal DB boveda_fiscal_sftp.proto Bóveda Fiscal SFTP (532 líneas)Ciclo de vida de una petición (v2 ExecuteQuery)
sequenceDiagram
participant C as Cliente gRPC
participant H as QueryHandlerV2
participant S as QueryExecutionService
participant CR as CredentialsRepository
participant DB as DatabaseRepository
C->>H: ExecuteQueryRequest(rfc, sql, pagination)
H->>H: IsValidRFC(rfc)
H->>H: Registrar query activo (queryID, cancelFunc)
H->>H: context.WithTimeout(tenantTimeout)
H->>S: Execute(ctx, request)
S->>CR: GetClientDatabaseConfig(ctx, rfc)
CR->>CR: Verificar caché (TTL 15 min)
alt Cache miss
CR->>CR: SELECT credenciales FROM ADMON.CatUsuarios
CR->>CR: Almacenar en caché con expiry
end
CR-->>S: ClientDatabaseConfig
S->>DB: ConnectToDatabase(ctx, config)
DB->>DB: sql.Open + PingContext (timeout 3s)
DB->>DB: COUNT(*) para paginación
DB->>DB: QueryContext con OFFSET/FETCH
DB-->>S: QueryResponse (rows, metadata, stats)
S-->>H: QueryResponse
H->>H: Eliminar query activo
H-->>C: ExecuteQueryResponse
Multiplexación de puerto con cmux
El servidor bind un único net.Listener en GRPC_PORT (default 50051) y lo pasa a cmux.New(). Dos matchers se configuran:
grpcListener = mux.MatchWithWriters( cmux.HTTP2MatchHeaderFieldSendSettings("content-type", "application/grpc"),)httpListener = mux.Match(cmux.Any())Los clientes gRPC envían peticiones HTTP/2 con content-type: application/grpc. cmux inspecciona este header y enruta la conexión al grpc.Server. Cualquier otra conexión —navegadores, agentes de health check— se enruta al http.Server que sirve la documentación HTML y el endpoint /health.
Un navegador que accede a http://host:50051 recibe la documentación interactiva del API. Un cliente gRPC que conecta al mismo puerto recibe el servicio completo. No se requiere ningún puerto adicional.
Composition root
La inyección de dependencias se realiza íntegramente en cmd/server/main.go. No existe un contenedor de DI: las dependencias se instancian de forma explícita y se pasan por constructor hacia abajo en el árbol de dependencias.
// main.go — ejemplo del wiringcredRepo := repository.NewCredentialsRepository(admonDB, cfg)dbRepo := repository.NewDatabaseRepository(logger)svc := services.NewQueryExecutionService(credRepo, dbRepo, logger)
handlerV1 := handlers.NewQueryHandler(svc, logger)handlerV2 := handlers.NewQueryHandlerV2(svc, logger)Este patrón mantiene el grafo de dependencias completamente visible y verificable sin herramientas adicionales.
Resumen
- Clean Architecture en tres capas: Handler → Service → Repository. Ninguna capa importa una capa de nivel superior.
- La estructura de directorios refleja las capas:
handlers/,services/,repository/tanto para el gateway SQL Server como para Bóveda Fiscal. - El ciclo de vida de una petición incluye validación RFC, caché de credenciales con TTL de 15 minutos, contexto con timeout propagado y registro de queries activos cancelables.
cmuxmultiplexia gRPC y HTTP/1.1 en el mismo puerto TCP. No hay puertos secundarios.- La inyección de dependencias es explícita en
main.go: sin contenedores, sin reflection, con grafo visible.