SHARED CONTRACTS AND PATTERNS

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

Shared Module Contracts and Patterns

Cross-Monorepo Alignment Document Last Updated: December 2025 Version: 1.0.0

This document outlines the shared contracts, patterns, and interfaces that both the editor and gograb-modules monorepos use for module development. Following these patterns ensures compatibility and enables seamless integration.


Table of Contents

  1. Core Module Contract
  2. Module Manifest Structure
  3. Embeddable Components
  4. Public Actions
  5. Module Routes
  6. Navigation Items
  7. Module Configuration
  8. Widget-Embeddable Bridge
  9. Best Practices

Core Module Contract

Every module must implement the BrowserModuleContract interface:

import { Observable } from 'rxjs';

export interface BrowserModuleContract {
  readonly manifest: ModuleManifest;
  
  configure<T = Record<string, unknown>>(config: T): Observable<void>;
  initialize(): Observable<void>;
  start(): Observable<void>;
  stop(): Observable<void>;
  destroy(): Observable<void>;
}

Lifecycle Methods

Method Purpose When Called
configure Apply runtime configuration Before initialization
initialize Set up services, state, subscriptions After configuration
start Begin active operations After initialization
stop Pause operations, cleanup resources Before destroy or deactivation
destroy Complete cleanup Module unload

Example Implementation

@ModuleDecorator({
  id: 'my-module',
  version: '1.0.0',
  name: 'My Module',
  // ... other metadata
})
@Injectable({ providedIn: 'root' })
export class MyModule implements BrowserModuleContract {
  private readonly logger = inject(LoggerService).child({ source: 'MyModule' });

  readonly manifest: ModuleManifest = {
    id: 'my-module',
    version: '1.0.0',
    name: 'My Module',
    basePath: 'my-module',
    // ... full manifest
  };

  configure<T>(config: T): Observable<void> {
    this.logger.info('Configuring module', { config });
    return of(void 0);
  }

  initialize(): Observable<void> {
    this.logger.info('Initializing module');
    return of(void 0);
  }

  start(): Observable<void> {
    this.logger.info('Starting module');
    return of(void 0);
  }

  stop(): Observable<void> {
    this.logger.info('Stopping module');
    return of(void 0);
  }

  destroy(): Observable<void> {
    this.logger.info('Destroying module');
    return of(void 0);
  }
}

Module Manifest Structure

The ModuleManifest interface defines all metadata about a module:

export interface ModuleManifest {
  // Required fields
  readonly id: string;
  readonly version: string;
  readonly name: string;
  readonly description?: string;
  readonly basePath?: string;  // URL prefix for routes

  // Category and organization
  readonly category?: ModuleCategory;
  readonly author?: string;
  readonly tags?: readonly string[];

  // Features
  readonly routes?: readonly ModuleRoute[];
  readonly embeddableComponents?: readonly ModuleEmbeddableComponent[];
  readonly publicActions?: readonly ModulePublicAction[];
  readonly permissions?: readonly ModulePermission[];
  readonly navigation?: ModuleNavigation;
  
  // Configuration schema
  readonly configuration?: ModuleConfigurationSchema;
  
  // AI metadata
  readonly metadata?: ModuleMetadata;
}

Categories

type ModuleCategory = 
  | 'core-tool'   // Essential platform tools
  | 'content'     // Content management
  | 'commerce'    // E-commerce features
  | 'analytics'   // Analytics and reporting
  | 'system'      // System administration
  | 'utility'     // Utility functions
  | 'custom';     // User-created

Embeddable Components

Embeddable components are Angular components that can be placed on visual pages:

export interface ModuleEmbeddableComponent {
  readonly id: string;
  readonly componentType: Type<unknown>;
  readonly displayName: string;
  readonly description?: string;
  readonly category?: string;
  readonly thumbnail?: string;
  readonly permissions?: readonly string[];
  readonly inputs?: readonly ModuleComponentInput[];
  readonly outputs?: readonly ModuleComponentOutput[];
}

Input Definition

export interface ModuleComponentInput {
  readonly name: string;
  readonly type: string;
  readonly label: string;
  readonly description?: string;
  readonly required?: boolean;
  readonly defaultValue?: unknown;
  readonly options?: readonly { value: unknown; label: string }[];
  readonly min?: number;
  readonly max?: number;
  readonly validation?: InputValidation;
}

Example Embeddable Component

// Component
@Component({
  selector: 'lib-product-card-widget',
  standalone: true,
  templateUrl: './product-card-widget.component.html',
})
export class ProductCardWidgetComponent {
  readonly productId = input.required<string>();
  readonly showAddToCart = input<boolean>(true);
  readonly size = input<'small' | 'medium' | 'large'>('medium');
}

// In manifest
embeddableComponents: [
  {
    id: 'ecommerce:product-card',
    componentType: ProductCardWidgetComponent,
    displayName: 'Product Card',
    description: 'Displays a single product',
    category: 'commerce',
    permissions: ['products:read'],
    inputs: [
      ParameterBuilders.string('productId')
        .required()
        .description('The ID of the product')
        .build(),
      ParameterBuilders.boolean('showAddToCart')
        .defaultValue(true)
        .description('Show add to cart button')
        .build(),
      ParameterBuilders.enum('size', ['small', 'medium', 'large'])
        .defaultValue('medium')
        .build(),
    ],
  },
]

Public Actions

Public actions are operations that can be invoked from other modules or widgets:

export interface ModulePublicAction {
  readonly id: string;
  readonly displayName: string;
  readonly description?: string;
  readonly parameters?: readonly ModuleActionParameter[];
  readonly returnType?: string;
  readonly permissions?: readonly string[];
}

Creating Actions

// Action class
export class CreateOrderAction implements ModulePublicAction {
  readonly id = 'ecommerce:create-order';
  readonly displayName = 'Create Order';
  readonly description = 'Create a new order from cart';
  readonly parameters = [
    ParameterBuilders.object('cart').required().build(),
    ParameterBuilders.object('shippingAddress').required().build(),
  ];
  readonly returnType = 'Order';
  readonly permissions = ['orders:create'];
}

// In manifest
publicActions: [
  new CreateOrderAction(),
  new AddToCartAction(),
]

Module Routes

Module routes define the navigation structure within a module:

export interface ModuleRoute {
  readonly path: string;
  readonly redirectTo?: string;
  readonly pathMatch?: 'full' | 'prefix' | 'slash';
  readonly component?: Type<unknown>;
  readonly loadComponent?: () => Promise<Type<unknown>>;
  readonly loadChildren?: () => Promise<readonly ModuleRoute[]>;
  readonly children?: readonly ModuleRoute[];
  readonly permissions?: readonly string[];
  readonly title?: string;
  readonly data?: Record<string, unknown>;
}

Example Routes

routes: [
  {
    path: '',
    redirectTo: 'products',
    pathMatch: 'full',
  },
  {
    path: 'products',
    loadComponent: () => import('./components/product-list.component')
      .then(m => m.ProductListComponent),
    title: 'Products',
    permissions: ['products:read'],
  },
  {
    path: 'products/:id',
    loadComponent: () => import('./components/product-detail.component')
      .then(m => m.ProductDetailComponent),
    title: 'Product Details',
  },
  {
    path: 'cart',
    loadComponent: () => import('./components/cart.component')
      .then(m => m.CartComponent),
    permissions: ['cart:manage'],
  },
]

Navigation Items

Navigation items define how the module appears in menus:

export interface ModuleNavigation {
  readonly menuItems: readonly ModuleMenuItem[];
}

export interface ModuleMenuItem {
  readonly id: string;
  readonly label: string;
  readonly icon?: string;
  readonly route: string;
  readonly order?: number;
  readonly permissions?: readonly string[];
  readonly children?: readonly ModuleMenuItem[];
}

Example Navigation

navigation: {
  menuItems: [
    {
      id: 'ecommerce-main',
      label: 'Shop',
      icon: 'shopping-bag',
      route: 'products',
      order: 200,
      permissions: ['products:read'],
      children: [
        {
          id: 'ecommerce-products',
          label: 'Products',
          icon: 'grid',
          route: 'products',
          order: 10,
        },
        {
          id: 'ecommerce-cart',
          label: 'Cart',
          icon: 'shopping-cart',
          route: 'cart',
          order: 20,
        },
        {
          id: 'ecommerce-orders',
          label: 'Orders',
          icon: 'receipt',
          route: 'orders',
          order: 30,
        },
      ],
    },
  ],
}

Module Configuration

Modules can define configuration schemas for runtime settings:

export interface ModuleConfigurationSchema {
  [key: string]: ConfigurationField;
}

export interface ConfigurationField {
  type: 'string' | 'number' | 'boolean' | 'select' | 'object';
  label: string;
  description?: string;
  default?: unknown;
  required?: boolean;
  options?: unknown[];  // For select type
  min?: number;         // For number type
  max?: number;         // For number type
}

Example Configuration

configuration: {
  defaultCurrency: {
    type: 'select',
    label: 'Default Currency',
    default: 'USD',
    options: ['USD', 'EUR', 'GBP'],
    required: true,
  },
  enableGuestCheckout: {
    type: 'boolean',
    label: 'Enable Guest Checkout',
    default: true,
  },
  taxRate: {
    type: 'number',
    label: 'Tax Rate (%)',
    default: 5,
    min: 0,
    max: 25,
  },
}

Widget-Embeddable Bridge

The editor monorepo provides a bridge between widgets and embeddable components:

How It Works

  1. Embeddable Registration: When modules are loaded, their embeddable components are registered with the widget system
  2. Widget Conversion: ModuleEmbeddableComponent instances are converted to WidgetDefinition objects
  3. Host Component: An EmbeddableHostWidgetComponent renders the actual embeddable component
  4. Input Mapping: Widget properties are mapped to embeddable inputs

ModuleWidgetBridgeService

@Injectable({ providedIn: 'root' })
export class ModuleWidgetBridgeService {
  // Convert embeddable to widget definition
  toWidgetDefinition(embeddable: ModuleEmbeddableComponent): WidgetDefinition;
  
  // Extract inputs from widget for embeddable
  widgetToEmbeddableInputs(widget: Widget): Record<string, unknown>;
  
  // Register all embeddables from a module as widgets
  registerEmbeddablesFromModule(module: BrowserModuleContract): Observable<void>;
  
  // Register all module embeddables at startup
  registerAllModuleEmbeddablesAsWidgets(): Observable<void>;
}

Using the Bridge

  1. Automatic Registration: The WidgetRegistryService automatically syncs embeddables from modules
  2. Manual Registration: Use ModuleWidgetBridgeService.registerEmbeddablesFromModule(module)
  3. Widget Palette: Embeddables appear in the widget library for drag-and-drop

Best Practices

1. Module Organization

modules/my-module/
├── src/
│   ├── lib/
│   │   ├── models/          # Data models and DTOs
│   │   ├── services/        # Business logic
│   │   ├── components/      # Full-page components
│   │   ├── embeddables/     # Embeddable widgets
│   │   ├── actions/         # Public actions
│   │   ├── state/           # State management (if needed)
│   │   ├── repositories/    # Data access
│   │   └── my-module.module.ts
│   └── index.ts             # Public API
├── project.json
└── package.json

2. Naming Conventions

3. Versioning

Follow semantic versioning (SemVer):

4. Permissions

Define granular permissions:

permissions: [
  { id: 'products:read', resource: 'products', action: 'read' },
  { id: 'products:create', resource: 'products', action: 'create' },
  { id: 'products:update', resource: 'products', action: 'update' },
  { id: 'products:delete', resource: 'products', action: 'delete' },
  { id: 'products:manage', resource: 'products', action: 'manage' }, // All permissions
]

5. Storage Integration

Use the shared storage system for offline-first data:

@Injectable({ providedIn: 'root' })
export class ProductService {
  private readonly storage = inject(GgStorage);
  
  products = autoFlashState<Product[]>('ecommerce:products', []);
}

6. Logging

Use structured logging with the shared log system:

private readonly logger = inject(LoggerService).child({ source: 'MyService' });

this.logger.info('Operation completed', { productId, count });
this.logger.error('Operation failed', { error });

7. Error Handling

Use RxJS for async operations:

createProduct(dto: CreateProductDto): Observable<Product> {
  return this.api.post<Product>('/products', dto).pipe(
    tap(product => this.logger.info('Product created', { id: product.id })),
    catchError(error => {
      this.logger.error('Failed to create product', { error });
      return throwError(() => new Error('Failed to create product'));
    }),
  );
}

Cross-Monorepo Compatibility

To ensure compatibility between editor and gograb-modules:

  1. Use shared types from @libs-core/module-base
  2. Follow the same manifest structure
  3. Use consistent naming conventions
  4. Keep embeddable interfaces compatible
  5. Use the same parameter builder utilities
  6. Follow the same lifecycle patterns

When developing in gograb-modules, the same patterns apply. Modules can be shared between monorepos by:

  1. Publishing to a shared npm registry
  2. Using Git submodules
  3. Copying module code with namespace adjustments

Migration Guide

From gograb-modules to editor

  1. Copy the module directory structure
  2. Update imports to use @libs-core/module-base path
  3. Ensure @ModuleDecorator is applied
  4. Register the module in the application config
  5. Test embeddable-to-widget conversion

From editor to gograb-modules

  1. Copy the module directory structure
  2. Update imports to use local paths
  3. Remove widget-specific code if any
  4. Ensure manifest compatibility
  5. Test module lifecycle

Support and Resources


This document is maintained by both the editor and gograb-modules teams. Updates should be synced between monorepos.