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
- Core Module Contract
- Module Manifest Structure
- Embeddable Components
- Public Actions
- Module Routes
- Navigation Items
- Module Configuration
- Widget-Embeddable Bridge
- 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
- Embeddable Registration: When modules are loaded, their embeddable components are registered with the widget system
- Widget Conversion:
ModuleEmbeddableComponentinstances are converted toWidgetDefinitionobjects - Host Component: An
EmbeddableHostWidgetComponentrenders the actual embeddable component - 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
- Automatic Registration: The
WidgetRegistryServiceautomatically syncs embeddables from modules - Manual Registration: Use
ModuleWidgetBridgeService.registerEmbeddablesFromModule(module) - 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
- Module ID: kebab-case, unique across all modules (e.g.,
user-management) - Embeddable ID: namespace:component (e.g.,
ecommerce:product-card) - Action ID: namespace:action (e.g.,
ecommerce:create-order) - Permission ID: resource:action (e.g.,
products:read)
3. Versioning
Follow semantic versioning (SemVer):
- Major: Breaking changes to manifest or public API
- Minor: New features, backward compatible
- Patch: Bug fixes, no API changes
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:
- Use shared types from
@libs-core/module-base - Follow the same manifest structure
- Use consistent naming conventions
- Keep embeddable interfaces compatible
- Use the same parameter builder utilities
- Follow the same lifecycle patterns
When developing in gograb-modules, the same patterns apply. Modules can be shared between monorepos by:
- Publishing to a shared npm registry
- Using Git submodules
- Copying module code with namespace adjustments
Migration Guide
From gograb-modules to editor
- Copy the module directory structure
- Update imports to use
@libs-core/module-basepath - Ensure
@ModuleDecoratoris applied - Register the module in the application config
- Test embeddable-to-widget conversion
From editor to gograb-modules
- Copy the module directory structure
- Update imports to use local paths
- Remove widget-specific code if any
- Ensure manifest compatibility
- Test module lifecycle
Support and Resources
- Documentation:
docs/english/GoGrab-Space/modules/ - Module Examples:
modules/new-user-management/,modules/ecommerce/ - Contract Interfaces:
libs/core/module-base/src/lib/contracts/ - Bridge Service:
libs/core/module-adapter/src/lib/
This document is maintained by both the editor and gograb-modules teams. Updates should be synced between monorepos.