Skip to content

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 wiring
credRepo := 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.
  • cmux multiplexia 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.