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
- Extensibilidad: Sistema fácilmente extensible para nuevos tipos de fuentes
- Type Safety: Type safety completo en tiempo de compilación
- Performance: Resolución rápida de bindings sin bloquear UI
- Simplicidad: API clara y fácil de usar para desarrolladores
- 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:
- Resolver bindings individuales o múltiples
- Seleccionar resolver apropiado
- Coordinar operaciones asíncronas
- Manejar errores con fallbacks
- Logging y observabilidad
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:
- ManualResolver: Retorna valor directo
resolve<T>(binding: DataBinding): Observable<T> {
const config = binding.sourceConfig as ManualSourceConfig;
return of(config.value as T);
}
- 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$;
}
- 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:
- ✅ Reutilización de validadores y UI hints
- ✅ Compatibilidad con sistema existente
- ✅ DRY - no duplicar metadata
- ✅ Type safety preservado
Consecuencias:
- ✅ Integración natural con widgets
- ✅ Menos código duplicado
- ⚠️ Dependencia de shared-models
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:
- ✅ Type narrowing automático en TypeScript
- ✅ No necesita type guards manuales
- ✅ IntelliSense completo
- ✅ Errores en tiempo de compilación
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:
- ✅ API consistente
- ✅ Compatible con RxJS pipelines
- ✅ Fácil composición
- ✅ Cancelación automática
- ✅ Alineado con Angular patterns
Consecuencias:
- ✅ Consistencia en toda la librería
- ✅ Preparado para async operations
- ⚠️ Ligero overhead para valores síncronos (mitigado con
of())
ADR-004: Resolver Pattern
Contexto: Necesitamos extensibilidad para nuevos tipos de fuentes.
Decisión: Usar Strategy Pattern con resolvers intercambiables.
Razones:
- ✅ Open-Closed Principle
- ✅ Fácil agregar nuevos tipos
- ✅ Testeable de forma aislada
- ✅ Single Responsibility
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:
- ✅ No rompe UI por errores de binding
- ✅ Comportamiento predecible
- ✅ Usa metadata existente
- ✅ Permite debugging con logs
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:
- ✅ Alineado con Angular 20 best practices
- ✅ Performance superior (fine-grained reactivity)
- ✅ Mejor developer experience
- ✅ OnPush compatible
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
- Input sanitization en configurador UI
- Validación de nombres de repositorio/método
- Safe navigation con optional chaining
- No eval() - nunca ejecutar strings como código
📈 Performance
Optimizaciones
- Lazy Resolution: Solo resolver bindings cuando se necesitan
- Parallel Resolution:
resolveMany()usaforkJoinpara paralelizar - Memoization: Cache de resultados para bindings idénticos (futuro)
- OnPush: Todos los componentes usan OnPush change detection
- Signals: Fine-grained reactivity sin zone.js overhead
Métricas Target
- Resolución manual: < 10ms
- Resolución repository: < 100ms (promedio)
- Resolución config: < 50ms
- Resolución paralela (10 bindings): < 200ms
🧪 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)
✅ Fase 1: Modelos y tipos
- DataBinding interface (libs/core/data-binding/src/lib/models/)
- SourceConfig types (manual, repository, config)
- Type utilities y validación
✅ Fase 2: Resolvers
- Base resolver abstracto
- Manual resolver (con validación de tipos)
- Repository resolver (con RepositoryRegistry)
- Config resolver (con ContextService)
✅ Fase 3: Service principal
- DataBindingService con caching
- Sistema de estadísticas (Signal-based)
- Error handling con fallback a defaultValue
- Logging integrado
⏳ Fase 4: UI (Pendiente - Fase 2)
- BindingConfiguratorComponent
- Styling
- Validación en tiempo real
✅ 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:
- ✅ Arquitectura base con Resolver Pattern
- ✅ Type safety completo (zero
anytypes) - ✅ Observable-based API con RxJS
- ✅ Caching con TTL configurable
- ✅ Estadísticas de resolución (Signals)
- ✅ Integración con ContextService (multi-tenant)
- ✅ Build exitoso (1777ms, zero errors)
- ✅ Ejemplos funcionales documentados
Pendiente (Fase 2):
- ⏳ UI Configurator Component
- ⏳ Integración con module-base
- ⏳ Ejemplo real en user-management
- ⏳ Mejorar cobertura de tests a ≥90%
Puntos de Atención (Logrados)
- ✅ Type Safety: Validación con
tsc --noEmit- zero errores - ✅ Performance: Caching implementado, benchmarks pendientes
- ⏳ UX: Configurador pendiente para Fase 2
- ✅ Extensibilidad: Interfaces preparadas para Singularity (Event/Action/Transform)
Próximo: Ver tasks.md para plan de implementación detallado.