Design

Ecosistema completo para organización y gestión de contenido con interfaces espaciales

Data Binding - Diseño Arquitectónico

📋 Información del Documento

Proyecto: GoGrab Space - Data Binding Library Versión: 1.0.0 (MVP) Fecha Inicio: 2025-01-04 Fecha Completado: 2025-01-05 Estado: ✅ MVP Implementado


🎯 Objetivos de Diseño

  1. Extensibilidad: Sistema fácilmente extensible para nuevos tipos de fuentes
  2. Type Safety: Type safety completo en tiempo de compilación
  3. Performance: Resolución rápida de bindings sin bloquear UI
  4. Simplicidad: API clara y fácil de usar para desarrolladores
  5. Preparación Futura: Diseño listo para integración con Singularity

🏗️ Arquitectura General

Diagrama de Componentes

┌─────────────────────────────────────────────────────────────────┐
│                      @libs-core/data-binding                    │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ┌────────────────┐         ┌──────────────────────┐          │
│  │   UI Layer     │         │   Service Layer      │          │
│  │                │         │                      │          │
│  │ ┌────────────┐ │         │ ┌────────────────┐  │          │
│  │ │  Binding   │ │ uses    │ │ DataBinding    │  │          │
│  │ │Configurator│ │────────▶│ │   Service      │  │          │
│  │ └────────────┘ │         │ └────────┬───────┘  │          │
│  └────────────────┘         │          │          │          │
│                             │          │          │          │
│  ┌────────────────┐         │          ▼          │          │
│  │  Model Layer   │         │ ┌────────────────┐  │          │
│  │                │         │ │   Resolver     │  │          │
│  │ ┌────────────┐ │         │ │   Registry     │  │          │
│  │ │ DataBinding│ │         │ └────────┬───────┘  │          │
│  │ │ (interface)│ │         │          │          │          │
│  │ └────────────┘ │         │          │          │          │
│  │ ┌────────────┐ │         │          ▼          │          │
│  │ │SourceConfig│ │         │ ┌────────────────┐  │          │
│  │ │   (types)  │ │         │ │   Resolvers    │  │          │
│  │ └────────────┘ │         │ │                │  │          │
│  └────────────────┘         │ │ ┌────────────┐ │  │          │
│                             │ │ │  Manual    │ │  │          │
│                             │ │ └────────────┘ │  │          │
│                             │ │ ┌────────────┐ │  │          │
│                             │ │ │ Repository │ │  │          │
│                             │ │ └────────────┘ │  │          │
│                             │ │ ┌────────────┐ │  │          │
│                             │ │ │   Config   │ │  │          │
│                             │ │ └────────────┘ │  │          │
│                             │ └────────────────┘  │          │
│                             └──────────────────────┘          │
└─────────────────────────────────────────────────────────────────┘
         │                              │                  │
         │                              │                  │
         ▼                              ▼                  ▼
┌────────────────┐         ┌──────────────────┐  ┌────────────────┐
│ @libs-core/    │         │ @libs-core/      │  │ @libs-core/    │
│ shared-models  │         │ data-access      │  │ log-system     │
└────────────────┘         └──────────────────┘  └────────────────┘

📦 Estructura de Paquete

libs/core/data-binding/
├── src/
│   ├── lib/
│   │   ├── models/                           # Modelos de dominio
│   │   │   ├── data-binding.interface.ts     # DataBinding principal
│   │   │   ├── binding-source.types.ts       # Tipos de fuentes
│   │   │   ├── source-configs/               # Configs por tipo
│   │   │   │   ├── manual-source.config.ts
│   │   │   │   ├── repository-source.config.ts
│   │   │   │   ├── config-source.config.ts
│   │   │   │   ├── event-source.config.ts    # Futuro
│   │   │   │   ├── action-source.config.ts   # Futuro
│   │   │   │   └── transform-source.config.ts # Futuro
│   │   │   └── binding-result.interface.ts   # Resultado de resolución
│   │   │
│   │   ├── services/                         # Servicios
│   │   │   ├── data-binding.service.ts       # Servicio principal
│   │   │   ├── binding-registry.service.ts   # Registro de bindings
│   │   │   └── resolvers/                    # Resolvers por tipo
│   │   │       ├── base.resolver.ts          # Clase abstracta base
│   │   │       ├── resolver.interface.ts     # Contrato de resolver
│   │   │       ├── manual.resolver.ts        # Valores estáticos
│   │   │       ├── repository.resolver.ts    # Data-access
│   │   │       └── config.resolver.ts        # Configuración
│   │   │
│   │   ├── ui/                               # Componentes UI
│   │   │   └── binding-configurator/
│   │   │       ├── binding-configurator.component.ts
│   │   │       ├── binding-configurator.component.html
│   │   │       ├── binding-configurator.component.scss
│   │   │       └── binding-configurator.component.spec.ts
│   │   │
│   │   └── utils/                            # Utilidades
│   │       ├── binding-validators.ts         # Validadores
│   │       └── type-converters.ts            # Conversión de tipos
│   │
│   └── index.ts                              # Barrel export
│
└── tests/                                    # Tests
    ├── integration/
    │   ├── binding-resolution.test.ts
    │   └── ui-configuration.test.ts
    └── fixtures/
        └── sample-bindings.ts

🎨 Modelo de Datos

Jerarquía de Tipos

// Base: ParameterMetadata (de @libs-core/shared-models)
interface ParameterMetadata {
  name: string;
  type: ParameterType;
  required: boolean;
  defaultValue?: unknown;
  description?: string;
  validators?: ParameterValidator[];
  example?: unknown;
  uiHints?: ParameterUIHints;
}

// Extensión: DataBinding
interface DataBinding extends ParameterMetadata {
  source: BindingSourceType;
  sourceConfig: BindingSourceConfig;
}

// Union Type Discriminado
type BindingSourceConfig =
  | ManualSourceConfig
  | RepositorySourceConfig
  | ConfigSourceConfig
  | EventSourceConfig      // Futuro
  | ActionSourceConfig     // Futuro
  | TransformSourceConfig; // Futuro

Diagrama de Tipos

                    ParameterMetadata
                           │
                           │ extends
                           ▼
                     DataBinding
                           │
                           │ has
                           ▼
                  BindingSourceConfig
                           │
              ┌────────────┴────────────┬──────────┬─────────────┐
              │                         │          │             │
              ▼                         ▼          ▼             ▼
    ManualSourceConfig     RepositorySourceConfig ConfigSourceConfig  [Future]
         {type, value}     {type, entity, method}  {type, key}

🔧 Diseño de Servicios

DataBindingService

Responsabilidades:

API Pública:

@Injectable({ providedIn: 'root' })
export class DataBindingService {
  /**
   * Resuelve un binding individual
   */
  resolve<T = unknown>(binding: DataBinding): Observable<T>;

  /**
   * Resuelve múltiples bindings en paralelo
   */
  resolveMany(bindings: DataBinding[]): Observable<Map<string, unknown>>;

  /**
   * Valida un binding sin resolverlo
   */
  validate(binding: DataBinding): ValidationResult;

  /**
   * Registra un resolver personalizado
   */
  registerResolver(type: string, resolver: Resolver): void;
}

Flujo de Resolución:

resolve(binding)
    │
    ├─▶ validate(binding)
    │       │
    │       ├─▶ Check required fields
    │       └─▶ Check source config
    │
    ├─▶ getResolver(binding.source)
    │       │
    │       └─▶ Return appropriate resolver
    │
    ├─▶ resolver.resolve(binding)
    │       │
    │       ├─▶ Execute resolution logic
    │       ├─▶ Handle errors
    │       └─▶ Return Observable<T>
    │
    └─▶ catchError → fallback to defaultValue

Resolver Pattern

Diseño:

/**
 * Base resolver interface
 */
interface Resolver {
  resolve<T>(binding: DataBinding): Observable<T>;
}

/**
 * Abstract base class con helpers comunes
 */
abstract class BaseResolver implements Resolver {
  protected logger = inject(LoggerService);

  abstract resolve<T>(binding: DataBinding): Observable<T>;

  protected extractValue(data: unknown, path: string): unknown {
    return path.split('.').reduce((obj, key) => obj?.[key], data);
  }

  protected validateType(value: unknown, expectedType: ParameterType): boolean {
    // Validación de tipo
  }
}

Resolvers Concretos:

  1. ManualResolver: Retorna valor directo
resolve<T>(binding: DataBinding): Observable<T> {
  const config = binding.sourceConfig as ManualSourceConfig;
  return of(config.value as T);
}
  1. RepositoryResolver: Invoca repositorio
resolve<T>(binding: DataBinding): Observable<T> {
  const config = binding.sourceConfig as RepositorySourceConfig;

  // 1. Get repository
  const repo = this.registry.get(config.entityName);

  // 2. Invoke method
  const method = repo[config.method];
  const result$ = method.apply(repo, config.methodParams || []);

  // 3. Extract value if path specified
  if (config.resultPath) {
    return result$.pipe(
      map(data => this.extractValue(data, config.resultPath))
    );
  }

  return result$;
}
  1. ConfigResolver: Lee configuración
resolve<T>(binding: DataBinding): Observable<T> {
  const config = binding.sourceConfig as ConfigSourceConfig;
  return this.configService.getValue(
    config.configKey,
    config.moduleId
  );
}

🎨 Diseño de UI

BindingConfiguratorComponent

Arquitectura del Componente:

BindingConfiguratorComponent
    │
    ├─▶ Input<string> parameterName
    ├─▶ Input<ParameterType> parameterType
    ├─▶ Input<string?> description
    ├─▶ Input<DataBinding?> currentBinding
    │
    ├─▶ Output<DataBinding> bindingConfigured
    ├─▶ Output<void> cancelled
    │
    ├─▶ Signal<BindingSourceType> selectedSource
    ├─▶ Signal<unknown> testResult
    │
    ├─▶ onSourceChange(): void
    ├─▶ onSave(): void
    ├─▶ onCancel(): void
    └─▶ onTest(): void

Flujo de Interacción:

User Opens Configurator
    │
    ├─▶ Display parameter info
    │
    ├─▶ User selects source type
    │       │
    │       └─▶ Render source-specific form
    │
    ├─▶ User fills configuration
    │
    ├─▶ [Optional] User tests binding
    │       │
    │       └─▶ resolve() → display result
    │
    └─▶ User saves
            │
            ├─▶ buildBinding()
            ├─▶ validate()
            └─▶ emit bindingConfigured

Estados del Componente:

type ConfiguratorState =
  | 'idle'          // Esperando interacción
  | 'configuring'   // Usuario editando
  | 'testing'       // Probando binding
  | 'saving'        // Guardando
  | 'error';        // Error de validación

🔄 Flujos de Datos

Flujo 1: Widget Inicialización

Widget Component ngOnInit()
    │
    ├─▶ Get binding from @Input()
    │
    ├─▶ DataBindingService.resolve(binding)
    │       │
    │       ├─▶ Validate binding
    │       ├─▶ Select resolver
    │       ├─▶ Resolve value
    │       └─▶ Return Observable<T>
    │
    └─▶ Subscribe → Update Signal
            │
            └─▶ UI re-renders (OnPush)

Flujo 2: Configuración de Binding

User Opens Binding Configurator
    │
    ├─▶ Display current binding (if exists)
    │
    ├─▶ User selects source type
    │       │
    │       └─▶ Render appropriate form
    │
    ├─▶ User configures source
    │
    ├─▶ [Optional] User tests
    │       │
    │       └─▶ resolve() → show result
    │
    ├─▶ User saves
    │       │
    │       ├─▶ Build DataBinding object
    │       ├─▶ Validate
    │       └─▶ Emit to parent
    │
    └─▶ Parent saves to widget config

Flujo 3: Action Execution

Action Execute Context
    │
    ├─▶ Get parameter bindings
    │
    ├─▶ DataBindingService.resolveMany(bindings)
    │       │
    │       └─▶ Parallel resolution
    │               │
    │               ├─▶ Manual bindings → immediate
    │               ├─▶ Repository bindings → async
    │               └─▶ Config bindings → async
    │
    ├─▶ Wait for all resolutions (forkJoin)
    │
    ├─▶ Build parameter map
    │
    └─▶ Execute action with resolved params

🎯 Decisiones Arquitectónicas

ADR-001: Extensión de ParameterMetadata

Contexto: Necesitamos un modelo para bindings que sea compatible con el sistema de parámetros existente.

Decisión: DataBinding extiende ParameterMetadata de @libs-core/shared-models.

Razones:

Consecuencias:


ADR-002: Union Types Discriminados

Contexto: Necesitamos type safety para diferentes configuraciones de fuentes.

Decisión: Usar union types discriminados con propiedad type común.

Razones:

Ejemplo:

type SourceConfig =
  | { type: 'manual'; value: unknown }
  | { type: 'repository'; entityName: string; method: string }
  | { type: 'config'; configKey: string };

function process(config: SourceConfig) {
  switch (config.type) {
    case 'manual':
      // TypeScript sabe que config.value existe
      return config.value;
    case 'repository':
      // TypeScript sabe que config.entityName existe
      return config.entityName;
  }
}

ADR-003: Observable-Based Resolution

Contexto: Resolución de bindings puede ser síncrona o asíncrona.

Decisión: Siempre retornar Observable<T>, incluso para valores síncronos.

Razones:

Consecuencias:


ADR-004: Resolver Pattern

Contexto: Necesitamos extensibilidad para nuevos tipos de fuentes.

Decisión: Usar Strategy Pattern con resolvers intercambiables.

Razones:

Implementación:

interface Resolver {
  resolve<T>(binding: DataBinding): Observable<T>;
}

class DataBindingService {
  private resolvers = new Map<BindingSourceType, Resolver>();

  registerResolver(type: BindingSourceType, resolver: Resolver) {
    this.resolvers.set(type, resolver);
  }

  resolve<T>(binding: DataBinding): Observable<T> {
    const resolver = this.resolvers.get(binding.source);
    return resolver.resolve<T>(binding);
  }
}

ADR-005: Fallback a DefaultValue

Contexto: ¿Qué hacer cuando la resolución falla?

Decisión: Usar defaultValue de ParameterMetadata como fallback.

Razones:

Implementación:

resolve<T>(binding: DataBinding): Observable<T> {
  return resolver.resolve<T>(binding).pipe(
    catchError(error => {
      this.logger.error('Binding resolution failed', error);

      if (binding.defaultValue !== undefined) {
        return of(binding.defaultValue as T);
      }

      throw error;
    })
  );
}

ADR-006: Signals para UI State

Contexto: ¿Cómo manejar estado reactivo en componentes?

Decisión: Usar Signals de Angular 20 para estado local.

Razones:

Ejemplo:

export class BindingConfiguratorComponent {
  selectedSource = signal<BindingSourceType>('manual');
  manualValue = signal<unknown>(null);
  testResult = signal<unknown>(null);

  // Computed signals
  isValid = computed(() => this.validateCurrentConfig());
}

📊 Diagramas de Secuencia

Secuencia: Resolución de Repository Binding

Widget           DataBindingService    RepositoryResolver    RepositoryRegistry    Repository
  │                      │                      │                     │                 │
  │  resolve(binding)    │                      │                     │                 │
  ├─────────────────────▶│                      │                     │                 │
  │                      │                      │                     │                 │
  │                      │  resolve(binding)    │                     │                 │
  │                      ├─────────────────────▶│                     │                 │
  │                      │                      │                     │                 │
  │                      │                      │   get('Config')     │                 │
  │                      │                      ├────────────────────▶│                 │
  │                      │                      │                     │                 │
  │                      │                      │   Repository        │                 │
  │                      │                      │◀────────────────────┤                 │
  │                      │                      │                     │                 │
  │                      │                      │  getValue('key')                      │
  │                      │                      ├──────────────────────────────────────▶│
  │                      │                      │                     │                 │
  │                      │                      │         Observable<value>              │
  │                      │                      │◀──────────────────────────────────────┤
  │                      │                      │                     │                 │
  │                      │  Observable<value>   │                     │                 │
  │                      │◀─────────────────────┤                     │                 │
  │                      │                      │                     │                 │
  │  Observable<value>   │                      │                     │                 │
  │◀─────────────────────┤                      │                     │                 │
  │                      │                      │                     │                 │
  │  subscribe()         │                      │                     │                 │
  ├──────────────────────┼──────────────────────┼─────────────────────┼─────────────────┤
  │                      │                      │                     │                 │
  │  signal.set(value)   │                      │                     │                 │
  └──────────────────────┘                      │                     │                 │

🔐 Seguridad

Validación de Tipos

// Validación en tiempo de ejecución
function validateBindingValue(
  value: unknown,
  expectedType: ParameterType
): boolean {
  switch (expectedType) {
    case 'string':
      return typeof value === 'string';
    case 'number':
      return typeof value === 'number' && !isNaN(value);
    case 'boolean':
      return typeof value === 'boolean';
    case 'date':
      return value instanceof Date || !isNaN(Date.parse(value as string));
    // ... otros tipos
  }
}

Sanitización


📈 Performance

Optimizaciones

  1. Lazy Resolution: Solo resolver bindings cuando se necesitan
  2. Parallel Resolution: resolveMany() usa forkJoin para paralelizar
  3. Memoization: Cache de resultados para bindings idénticos (futuro)
  4. OnPush: Todos los componentes usan OnPush change detection
  5. Signals: Fine-grained reactivity sin zone.js overhead

Métricas Target


🧪 Testabilidad

Estrategia de Testing

// Unit Tests: Resolvers aislados
describe('RepositoryResolver', () => {
  it('should resolve value from repository method', () => {
    const mockRepo = { getValue: () => of({ value: 20 }) };
    const mockRegistry = { get: () => mockRepo };

    const resolver = new RepositoryResolver(mockRegistry, logger);
    const binding: DataBinding = {...};

    resolver.resolve(binding).subscribe(value => {
      expect(value).toBe(20);
    });
  });
});

// Integration Tests: Flujo completo
describe('DataBindingService Integration', () => {
  it('should resolve repository binding end-to-end', () => {
    // Setup real services
    const service = TestBed.inject(DataBindingService);
    const binding: DataBinding = {...};

    service.resolve(binding).subscribe(value => {
      expect(value).toBeDefined();
    });
  });
});

📝 Notas de Implementación

✅ Orden de Implementación (Completado)

  1. ✅ Fase 1: Modelos y tipos

    • DataBinding interface (libs/core/data-binding/src/lib/models/)
    • SourceConfig types (manual, repository, config)
    • Type utilities y validación
  2. ✅ Fase 2: Resolvers

    • Base resolver abstracto
    • Manual resolver (con validación de tipos)
    • Repository resolver (con RepositoryRegistry)
    • Config resolver (con ContextService)
  3. ✅ Fase 3: Service principal

    • DataBindingService con caching
    • Sistema de estadísticas (Signal-based)
    • Error handling con fallback a defaultValue
    • Logging integrado
  4. ⏳ Fase 4: UI (Pendiente - Fase 2)

    • BindingConfiguratorComponent
    • Styling
    • Validación en tiempo real
  5. ✅ Fase 5: Testing & Docs

    • 96 unit tests (manual-resolver, config-resolver, data-binding-service)
    • 2 ejemplos funcionales (binding-usage, user-list-widget)
    • Documentación completa (README, requirements, design, tasks)

📊 Estado de Implementación

Completado:

Pendiente (Fase 2):

Puntos de Atención (Logrados)


Próximo: Ver tasks.md para plan de implementación detallado.