Manifest Mvp Plan

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

ModuleManifest MVP Update Plan

🎯 Directivas Core


1. Análisis Simplificado

Estado Actual (module-manifest.interface.ts)

export interface ModuleManifest {
  readonly id: string;
  readonly version: string;
  readonly displayName?: string;
  readonly permissions?: readonly string[];
}

Estado Objetivo MVP (module-base.md)

export interface ModuleManifest {
  // Core (SIMPLIFICADO)
  id: string;
  version: string;
  name: string; // displayName → name (requerido)
  description: string;

  // Navigation (ESENCIAL)
  navigation?: ModuleNavigationConfig;
  routes?: ModuleRoute[];

  // Components & Actions (CORE MVP)
  embeddableComponents?: ModuleEmbeddableComponent[];
  publicActions?: ModulePublicAction[];

  // Dependencies (MÍNIMO)
  dependencies?: ModuleDependency[];
  permissions: ModulePermission[]; // Siempre array estructurado

  // Metadata (OPCIONAL MVP)
  author?: string;
  tags?: string[];
}

Diferencias clave:


2. Interfaces MVP (Simplificadas)

ModuleManifest Principal

/**
 * Module manifest for MVP - simplified and focused
 */
export interface ModuleManifest {
  /** Unique module identifier */
  readonly id: string;

  /** Semantic version (e.g., '1.0.0') */
  readonly version: string;

  /** Display name for UI */
  readonly name: string;

  /** Brief description */
  readonly description: string;

  /** Navigation configuration */
  readonly navigation?: ModuleNavigationConfig;

  /** Module routes for Angular Router */
  readonly routes?: readonly ModuleRoute[];

  /** Components that can be embedded by other modules */
  readonly embeddableComponents?: readonly ModuleEmbeddableComponent[];

  /** Public actions exposed to other modules */
  readonly publicActions?: readonly ModulePublicAction[];

  /** Module dependencies */
  readonly dependencies?: readonly ModuleDependency[];

  /** Required permissions (structured) */
  readonly permissions: readonly ModulePermission[];

  /** Module author (optional) */
  readonly author?: string;

  /** Tags for categorization (optional) */
  readonly tags?: readonly string[];
}

NavigationConfig (con Signals)

export interface ModuleNavigationConfig {
  /** Main menu items */
  readonly menuItems: readonly NavigationMenuItem[];

  /** Breadcrumb configuration (optional) */
  readonly breadcrumbs?: readonly BreadcrumbConfig[];

  /** Toolbar items (optional) */
  readonly toolbar?: readonly ToolbarItem[];
}

export interface NavigationMenuItem {
  readonly id: string;
  readonly label: string;
  readonly icon?: string;
  readonly route?: string;
  readonly order: number;

  /** Badge count using Signal (Angular 20) */
  readonly badge?: Signal<number>; // ✅ Signal, no Observable

  /** Required permissions to view */
  readonly permissions?: readonly string[];

  /** Child menu items */
  readonly children?: readonly NavigationMenuItem[];
}

export interface BreadcrumbConfig {
  readonly label: string;
  readonly route?: string;
}

export interface ToolbarItem {
  readonly id: string;
  readonly label: string;
  readonly icon?: string;
  readonly action: string; // Action ID to execute
  readonly permissions?: readonly string[];
}

ModuleRoute (Angular Router)

export interface ModuleRoute {
  readonly path: string;
  readonly component: Type<unknown>; // ✅ No `any`, usar `unknown`
  readonly permissions?: readonly string[];
  readonly data?: Record<string, unknown>; // ✅ Typed object
}

ModuleEmbeddableComponent

export interface ModuleEmbeddableComponent {
  readonly id: string;
  readonly componentType: Type<unknown>; // ✅ No `any`
  readonly displayName: string;
  readonly description?: string;
  readonly category: 'widget' | 'list' | 'form' | 'dashboard';
  readonly permissions?: readonly string[];

  /** Expected inputs (typed) */
  readonly inputs?: readonly ComponentInput[];

  /** Preview thumbnail (optional) */
  readonly thumbnail?: string;
}

export interface ComponentInput {
  readonly name: string;
  readonly type: 'string' | 'number' | 'boolean' | 'object' | 'array';
  readonly required: boolean;
  readonly defaultValue?: unknown;
}

ModulePublicAction

export interface ModulePublicAction {
  readonly id: string;
  readonly displayName: string;
  readonly description?: string;
  readonly category: 'navigation' | 'crud' | 'workflow' | 'integration';
  readonly permissions?: readonly string[];

  /** Input schema (typed) */
  readonly inputSchema?: Record<string, unknown>;

  /** Output schema (typed) */
  readonly outputSchema?: Record<string, unknown>;
}

ModuleDependency

export interface ModuleDependency {
  readonly moduleId: string;
  readonly version: string;
  readonly required: boolean;
}

ModulePermission (desde @libs-core/shared-models o redefinir)

export interface ModulePermission {
  readonly id: string;
  readonly name: string;
  readonly description?: string;
  readonly category?: string;
}

3. Estrategia de Implementación MVP

Fase Única: Implementación Directa (Simplificado)

Sin fases complejas - implementación directa en 1-2 días

Día 1: Interfaces + Services

Mañana (4 horas): Interfaces

  1. ✅ Actualizar module-manifest.interface.ts (reemplazar, no extender)
  2. ✅ Crear navigation.interface.ts (NavigationConfig, MenuItem, Breadcrumb, Toolbar)
  3. ✅ Crear embeddable-component.interface.ts (ModuleEmbeddableComponent, ComponentInput)
  4. ✅ Crear public-action.interface.ts (ModulePublicAction)
  5. ✅ Crear module-dependency.interface.ts (ModuleDependency)
  6. ✅ Actualizar barrel src/index.ts

Tarde (4 horas): Services

  1. ✅ Actualizar ModuleRegistryService - procesar nuevas propiedades
  2. ✅ Crear NavigationService (básico) - agregar menús de módulos
  3. ✅ Crear ComponentRegistryService (básico) - registrar componentes embebibles
  4. ✅ Integrar con ActionRegistryService existente para public actions

Día 2: Testing + Docs

Mañana (4 horas): Testing

  1. ✅ Tests unitarios para interfaces (validación TypeScript)
  2. ✅ Tests para ModuleRegistryService con manifests completos
  3. ✅ Tests para NavigationService (agregación básica)
  4. ✅ Tests para ComponentRegistryService

Tarde (3 horas): Documentación

  1. ✅ JSDoc completo en todas las interfaces
  2. ✅ Ejemplo de manifest completo (Products module)
  3. ✅ Actualizar module-base.md con estado real

Total: ~15 horas (~2 días)


4. Decisiones de Diseño MVP

✅ Signals sobre Observables

// NavigationMenuItem
badge?: Signal<number>; // ✅ MVP usa Signals

// Si necesitamos convertir Observable a Signal:
const badgeCount = toSignal(observable$, { initialValue: 0 });

✅ Tipos Estrictos (No any)

// ❌ Evitar
componentType: Type<any>

// ✅ Usar
componentType: Type<unknown> // Más seguro, requiere validación

✅ Estructuras Simples

// Permissions siempre estructurado
permissions: readonly ModulePermission[]

// No strings directos
// permissions?: string[] // ❌

❌ Sin Features Innecesarias para MVP

Foco MVP: Navigation + Routes + Registro de componentes/acciones


5. Estructura de Archivos

Nuevos Archivos a Crear

/libs/core/module-base/src/lib/contracts/
├── module-manifest.interface.ts (REEMPLAZAR)
├── navigation.interface.ts (NUEVO)
├── embeddable-component.interface.ts (NUEVO)
├── public-action.interface.ts (NUEVO)
└── module-dependency.interface.ts (NUEVO)

/libs/core/module-base/src/lib/services/
├── module-registry.service.ts (ACTUALIZAR)
├── navigation.service.ts (NUEVO - básico)
└── component-registry.service.ts (NUEVO - básico)

/libs/core/module-base/tests/
├── manifest-interfaces.test.ts (NUEVO)
├── module-registry.test.ts (ACTUALIZAR)
├── navigation.test.ts (NUEVO)
└── component-registry.test.ts (NUEVO)

/docs/libs/module-base/
├── examples/
│   └── products-manifest.example.ts (NUEVO)
└── module-base.md (ACTUALIZAR)

Archivos a Actualizar

/libs/core/module-base/src/index.ts (barrel exports)
/libs/core/module-integrator/ (si usa ModuleManifest directamente)

6. Testing MVP (Pragmático)

Tests Esenciales

Unit Tests (interfaces):

describe('ModuleManifest MVP', () => {
  it('should compile with minimal required properties', () => {
    const manifest: ModuleManifest = {
      id: 'test-module',
      version: '1.0.0',
      name: 'Test Module',
      description: 'A test module',
      permissions: []
    };

    expect(manifest.id).toBe('test-module');
  });

  it('should accept full navigation config with Signals', () => {
    const badgeSignal = signal(5);

    const manifest: ModuleManifest = {
      id: 'test-module',
      version: '1.0.0',
      name: 'Test Module',
      description: 'A test module',
      permissions: [],
      navigation: {
        menuItems: [
          {
            id: 'item1',
            label: 'Item 1',
            route: '/item1',
            order: 1,
            badge: badgeSignal // ✅ Signal
          }
        ]
      }
    };

    expect(manifest.navigation?.menuItems[0].badge?.()).toBe(5);
  });
});

Integration Tests (services):

describe('NavigationService MVP', () => {
  it('should aggregate menu items from multiple modules', () => {
    const manifest1: ModuleManifest = {
      id: 'module1',
      version: '1.0.0',
      name: 'Module 1',
      description: 'First module',
      permissions: [],
      navigation: {
        menuItems: [{ id: 'item1', label: 'Item 1', route: '/item1', order: 1 }]
      }
    };

    const manifest2: ModuleManifest = {
      id: 'module2',
      version: '1.0.0',
      name: 'Module 2',
      description: 'Second module',
      permissions: [],
      navigation: {
        menuItems: [{ id: 'item2', label: 'Item 2', route: '/item2', order: 2 }]
      }
    };

    moduleRegistry.registerModule(manifest1);
    moduleRegistry.registerModule(manifest2);

    const menuItems = navigationService.getAllMenuItems();

    expect(menuItems).toHaveLength(2);
    expect(menuItems[0].id).toBe('item1');
    expect(menuItems[1].id).toBe('item2');
  });
});

Cobertura Objetivo: ≥80% (pragmático para MVP, no 90%)


7. Ejemplo Completo MVP

Products Module Manifest

import { signal } from '@angular/core';
import { ProductListComponent } from './components/product-list.component';
import { ProductListWidgetComponent } from './widgets/product-list-widget.component';

export const PRODUCTS_MODULE_MANIFEST: ModuleManifest = {
  id: 'products',
  version: '1.0.0',
  name: 'Product Management',
  description: 'Comprehensive product catalog and inventory management',

  // Navigation
  navigation: {
    menuItems: [
      {
        id: 'products-main',
        label: 'Products',
        icon: 'inventory',
        route: '/products',
        order: 10,
        badge: signal(0), // ✅ Signal para badge count
        permissions: ['products:read'],
        children: [
          {
            id: 'products-list',
            label: 'Product List',
            route: '/products/list',
            order: 1,
            permissions: ['products:read']
          },
          {
            id: 'products-create',
            label: 'New Product',
            route: '/products/create',
            order: 2,
            permissions: ['products:create']
          }
        ]
      }
    ],
    breadcrumbs: [
      { label: 'Home', route: '/' },
      { label: 'Products', route: '/products' }
    ],
    toolbar: [
      {
        id: 'products-export',
        label: 'Export',
        icon: 'download',
        action: 'products:export',
        permissions: ['products:export']
      }
    ]
  },

  // Routes
  routes: [
    {
      path: 'products',
      component: ProductListComponent,
      permissions: ['products:read']
    }
  ],

  // Embeddable Components
  embeddableComponents: [
    {
      id: 'product-list-widget',
      componentType: ProductListWidgetComponent,
      displayName: 'Product List Widget',
      description: 'Displays a list of products in widget format',
      category: 'widget',
      permissions: ['products:read'],
      inputs: [
        { name: 'maxItems', type: 'number', required: false, defaultValue: 10 },
        { name: 'category', type: 'string', required: false }
      ]
    }
  ],

  // Public Actions
  publicActions: [
    {
      id: 'products:get-by-id',
      displayName: 'Get Product by ID',
      description: 'Retrieve a single product by its ID',
      category: 'crud',
      permissions: ['products:read'],
      inputSchema: { productId: 'string' },
      outputSchema: { product: 'object' }
    },
    {
      id: 'products:export',
      displayName: 'Export Products',
      description: 'Export products to CSV/Excel',
      category: 'integration',
      permissions: ['products:export']
    }
  ],

  // Dependencies
  dependencies: [
    { moduleId: 'inventory', version: '^1.0.0', required: true },
    { moduleId: 'pricing', version: '^2.0.0', required: false }
  ],

  // Permissions
  permissions: [
    { id: 'products:read', name: 'Read Products', category: 'products' },
    { id: 'products:create', name: 'Create Products', category: 'products' },
    { id: 'products:update', name: 'Update Products', category: 'products' },
    { id: 'products:delete', name: 'Delete Products', category: 'products' },
    { id: 'products:export', name: 'Export Products', category: 'products' }
  ],

  // Metadata
  author: 'GoGrab Team',
  tags: ['ecommerce', 'inventory', 'catalog']
};

8. Servicios MVP (Implementación Básica)

NavigationService (Básico)

import { Injectable, Signal } from '@angular/core';
import { ModuleManifest, NavigationMenuItem } from '../contracts';

@Injectable({ providedIn: 'root' })
export class NavigationService {
  private modules = new Map<string, ModuleManifest>();

  registerModule(manifest: ModuleManifest): void {
    this.modules.set(manifest.id, manifest);
  }

  getAllMenuItems(): NavigationMenuItem[] {
    const allItems: NavigationMenuItem[] = [];

    for (const manifest of this.modules.values()) {
      if (manifest.navigation?.menuItems) {
        allItems.push(...manifest.navigation.menuItems);
      }
    }

    // Sort by order
    return allItems.sort((a, b) => a.order - b.order);
  }

  getMenuItemsWithPermissions(userPermissions: string[]): NavigationMenuItem[] {
    return this.getAllMenuItems().filter(item =>
      this.hasPermissions(item, userPermissions)
    );
  }

  private hasPermissions(item: NavigationMenuItem, userPermissions: string[]): boolean {
    if (!item.permissions || item.permissions.length === 0) {
      return true;
    }

    return item.permissions.every(perm => userPermissions.includes(perm));
  }
}

ComponentRegistryService (Básico)

import { Injectable, Type } from '@angular/core';
import { ModuleManifest, ModuleEmbeddableComponent } from '../contracts';

@Injectable({ providedIn: 'root' })
export class ComponentRegistryService {
  private components = new Map<string, ModuleEmbeddableComponent>();

  registerModule(manifest: ModuleManifest): void {
    if (manifest.embeddableComponents) {
      for (const component of manifest.embeddableComponents) {
        this.components.set(component.id, component);
      }
    }
  }

  getComponent(id: string): ModuleEmbeddableComponent | undefined {
    return this.components.get(id);
  }

  getComponentsByCategory(category: string): ModuleEmbeddableComponent[] {
    return Array.from(this.components.values()).filter(
      comp => comp.category === category
    );
  }

  getAllComponents(): ModuleEmbeddableComponent[] {
    return Array.from(this.components.values());
  }
}

ModuleRegistryService (Actualización)

import { Injectable } from '@angular/core';
import { ModuleManifest } from '../contracts';
import { NavigationService } from './navigation.service';
import { ComponentRegistryService } from './component-registry.service';
import { ActionRegistryService } from '@libs-core/action-registry';

@Injectable({ providedIn: 'root' })
export class ModuleRegistryService {
  private modules = new Map<string, ModuleManifest>();

  constructor(
    private navigationService: NavigationService,
    private componentRegistry: ComponentRegistryService,
    private actionRegistry: ActionRegistryService
  ) {}

  registerModule(manifest: ModuleManifest): void {
    // Validar manifest básico
    this.validateManifest(manifest);

    // Registrar módulo
    this.modules.set(manifest.id, manifest);

    // Registrar navigation
    if (manifest.navigation) {
      this.navigationService.registerModule(manifest);
    }

    // Registrar embeddable components
    if (manifest.embeddableComponents) {
      this.componentRegistry.registerModule(manifest);
    }

    // Registrar public actions
    if (manifest.publicActions) {
      for (const action of manifest.publicActions) {
        // Integration con ActionRegistryService existente
        this.actionRegistry.registerAction(manifest.id, action);
      }
    }
  }

  private validateManifest(manifest: ModuleManifest): void {
    if (!manifest.id || !manifest.version || !manifest.name) {
      throw new Error('Invalid manifest: id, version, and name are required');
    }

    // Validar dependencies si existen
    if (manifest.dependencies) {
      for (const dep of manifest.dependencies) {
        if (dep.required && !this.modules.has(dep.moduleId)) {
          throw new Error(`Required dependency not found: ${dep.moduleId}`);
        }
      }
    }
  }

  getModule(id: string): ModuleManifest | undefined {
    return this.modules.get(id);
  }

  getAllModules(): ModuleManifest[] {
    return Array.from(this.modules.values());
  }
}

9. Cronograma MVP

Día 1 (8 horas)

Día 2 (7 horas)

Total: 15 horas (~2 días)


10. Checklist de Implementación

Interfaces ✅

Services ✅

Testing ✅

Documentación ✅


11. Criterios de Éxito MVP

Funcional

Técnico

Documentación


12. Próximos Pasos Post-MVP

No implementar ahora, pero considerar para futuro:

Foco MVP: Registro + Navegación básica + Componentes/Acciones catalogados


Conclusión MVP

Este plan simplificado y pragmático se enfoca en:

Entregable: Sistema de manifests completo y funcional en 2 días (~15 horas)

Próximo paso: ¿Comenzar implementación directa de interfaces?