Skip to content

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'
}
CampoRuntime usage
slugLlave canónica. Sirve de FK semántica entre DB (available_connections.slug) y código (factories, AI tools).
displayName/icon/cover/descriptionCopiado a la DB por POST /available/seed — puede ser editado por admins vía PUT /available/:id.
authTypeDiscrimina el flujo de creación (ver Auth Discovery Flow).
configFactoryUsado por ConnectionConfigFactory.create(slug, raw) para validar y construir el VO de configuración.
formSchemaServido vía GET /available/:id/form-schema cuando authType === 'credentials'.
oauthScopesScopes 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). slug y authType NUNCA cambian una vez insertados — cambiar authType romperí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

  1. Crea el value-object de config en domain/value-objects/<slug>-config.vo.ts + su Zod schema en packages/schemas/src/connections/.
  2. Añade el factory en el registry (configFactory) y el formSchema si aplica.
  3. Añade el entry al array AVAILABLE_CONNECTIONS_REGISTRY con slug, displayName, authType, oauthScopes (si OAuth).
  4. Actualiza ConnectionSlug (union type) en el propio registry.
  5. Si es OAuth, añade la GoogleCapability o equivalente en domain/enums/connection.enums.ts.
  6. Llama POST /connections/available/seed en los ambientes que hagan falta para hidratar la fila.

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:

  1. Migrar/eliminar las conexiones que referencian el slug.
  2. Borrar la fila del catálogo (DELETE /available/:id, que exige count = 0 dependientes).
  3. Borrar el entry del registry.
  4. Deploy.

Invariantes que mantiene el registry

InvariantePor qué
slug del registry === available_connections.slug en DBPermite a availableRepo.findById(...).slug devolver un literal válido sin hacer mapping.
Todo entry con authType === 'oauth' tiene oauthScopes.length > 0StartGoogleOAuthCommand 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.

Páginas relacionadas