Appearance
Connections Registry Contract
apps/api/src/modules/connections/domain/registry/available-connections.registry.ts es la source of truth del código para todo lo que define un tipo de conexión. La tabla connections.available_connections es la proyección persistente de este registry.
Esta página explica el contrato y cómo mantener ambos lados sincronizados.
Qué describe el registry
Cada entrada IAvailableConnectionRegistryEntry contiene:
ts
interface IAvailableConnectionRegistryEntry {
readonly slug: ConnectionSlug; // 'smtp' | 'telegram' | 'gmail' | 'sheets' | 'calendar'
readonly displayName: string; // 'SMTP', 'Gmail', 'Google Calendar', ...
readonly authType: ConnectionAuthType; // 'oauth' | 'credentials'
readonly icon: string | null;
readonly cover: string | null;
readonly description: string | null;
readonly configFactory: (raw) => Result<IConnectionConfig>;
readonly formSchema: IAvailableConnectionFormSchema;
readonly oauthScopes?: readonly string[]; // requerido cuando authType === 'oauth'
}| Campo | Runtime usage |
|---|---|
slug | Llave canónica. Sirve de FK semántica entre DB (available_connections.slug) y código (factories, AI tools). |
displayName/icon/cover/description | Copiado a la DB por POST /available/seed — puede ser editado por admins vía PUT /available/:id. |
authType | Discrimina el flujo de creación (ver Auth Discovery Flow). |
configFactory | Usado por ConnectionConfigFactory.create(slug, raw) para validar y construir el VO de configuración. |
formSchema | Servido vía GET /available/:id/form-schema cuando authType === 'credentials'. |
oauthScopes | Scopes solicitados a Google en POST /oauth/start. El handler hace requireRegistryEntryBySlug(slug).oauthScopes. |
Cómo se sincroniza DB ↔ registry
El comando POST /connections/available/seed es idempotente:
ts
// seed-available-connections.command.ts — pseudocódigo
for (const entry of AVAILABLE_CONNECTIONS_REGISTRY) {
await repo.upsertBySlug({
slug: entry.slug,
displayName: entry.displayName,
authType: entry.authType,
icon: entry.icon,
cover: entry.cover,
description: entry.description,
});
}Reglas del upsert:
- Si la fila no existe → INSERT.
- Si la fila existe → UPDATE de los campos "display" (
displayName,icon,cover,description).slugyauthTypeNUNCA cambian una vez insertados — cambiarauthTyperompería el contrato con todas las conexiones existentes.
La migración SQL (ver runbooks/connections-redesign-migration.md) solo hace un backfill estructural. El admin DEBE llamar POST /available/seed después de correr la migración 1 para hidratar los campos display con los valores del registry vigente.
Cuándo editar el registry
Añadir un nuevo tipo de conexión
- Crea el value-object de config en
domain/value-objects/<slug>-config.vo.ts+ su Zod schema enpackages/schemas/src/connections/. - Añade el factory en el registry (
configFactory) y elformSchemasi aplica. - Añade el entry al array
AVAILABLE_CONNECTIONS_REGISTRYconslug,displayName,authType,oauthScopes(si OAuth). - Actualiza
ConnectionSlug(union type) en el propio registry. - Si es OAuth, añade la
GoogleCapabilityo equivalente endomain/enums/connection.enums.ts. - Llama
POST /connections/available/seeden los ambientes que hagan falta para hidratar la fila.
Editar un display del catálogo
NO edites el registry para cambios solo-display. Usa PUT /connections/available/:id en el panel admin. El registry vive en deploys; el catálogo en producción debe poder ajustar íconos o descripciones sin un release.
Excepción: si quieres cambiar el default que se aplica al primer seed de un ambiente nuevo, sí edita el registry.
Deprecar un slug
Borrar un entry del registry SIN antes borrar/migrar las conexiones que lo usan rompe el sistema — el handler fallará al resolver requireRegistryEntryBySlug. Pasos seguros:
- Migrar/eliminar las conexiones que referencian el slug.
- Borrar la fila del catálogo (
DELETE /available/:id, que exigecount = 0dependientes). - Borrar el entry del registry.
- Deploy.
Invariantes que mantiene el registry
| Invariante | Por qué |
|---|---|
slug del registry === available_connections.slug en DB | Permite a availableRepo.findById(...).slug devolver un literal válido sin hacer mapping. |
Todo entry con authType === 'oauth' tiene oauthScopes.length > 0 | StartGoogleOAuthCommand lee oauthScopes sin branching — el registry garantiza que nunca es undefined para OAuth. |
configFactory valida el input y devuelve un Result<IConnectionConfig> | El handler no ejecuta try/catch sobre Zod directamente; el error viaja por Result hasta el controller, que lo traduce a 400. |
formSchema.fields = [] cuando authType === 'oauth' | La UI renderiza vacío y redirige al flujo OAuth en lugar de intentar pintar un formulario. |
Tests que blindan el contrato
test/modules/connections/domain/registry/available-connections.registry.spec.ts— valida que cada entry tiene la shape correcta, que OAuth entries tienen scopes y que el set de slugs matchea la union type.test/modules/connections/presentation/available-connections-lifecycle.spec.ts— valida el flujo end-to-end a nivel HTTP (seed idempotente, form-schema branching, 409 on in-use delete).test/e2e/connections/catalog-lifecycle.e2e.spec.ts— lifecycle completo con mocked buses.