README

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

@libs-core/action-registry

Comprehensive action registry and execution system for Angular 20 applications with Command Pattern implementation

📋 Overview

@libs-core/action-registry is a robust, scalable action management system that implements the Command Pattern for Angular 20 applications. It provides centralized registration, discovery, and execution of actions with built-in support for permissions, validation, circuit breaking, and event-driven architecture.

Key Features


🚀 Quick Start

Installation

The library is part of the GoGrab Space monorepo. No separate installation required.

// Import in your Angular application
import { ActionRegistryService, ActionHandlerService } from '@libs-core/action-registry';

Basic Usage

1. Register an Action

import { Injectable, inject } from '@angular/core';
import { Observable, of } from 'rxjs';
import {
  ExecutableAction,
  ActionParams,
  ActionContext,
  PublicAction
} from '@libs-core/action-registry';

@PublicAction({
  id: 'hello-world',
  name: 'Hello World',
  description: 'Simple hello world action',
  permissions: [],
  tags: ['demo', 'basic']
})
@Injectable({ providedIn: 'root' })
export class HelloWorldAction implements ExecutableAction<string> {
  execute(params: ActionParams, context: ActionContext): Observable<string> {
    return of(`Hello ${params['name'] || 'World'}!`);
  }
}

2. Register the Action with ActionRegistryService

import { Component, inject, OnInit } from '@angular/core';
import { ActionRegistryService } from '@libs-core/action-registry';
import { HelloWorldAction } from './actions/hello-world.action';

@Component({
  selector: 'app-root',
  standalone: true,
  template: `<h1>Action Registry Demo</h1>`
})
export class AppComponent implements OnInit {
  private readonly actionRegistry = inject(ActionRegistryService);

  ngOnInit(): void {
    // Register action
    this.actionRegistry.register({
      id: 'demo.hello-world',
      actionClass: HelloWorldAction,
      metadata: {
        name: 'Hello World',
        description: 'Simple hello world action',
        type: 'custom',
        permissions: [],
        tags: ['demo'],
        isAsync: true,
        timeout: 5000,
        retryable: false,
        maxRetries: 0
      }
    });
  }
}

3. Execute the Action

import { Component, inject } from '@angular/core';
import { ActionHandlerService } from '@libs-core/action-registry';

@Component({
  selector: 'app-demo',
  standalone: true,
  template: `
    <button (click)="executeAction()">Execute Action</button>
    <p>{{ result }}</p>
  `
})
export class DemoComponent {
  private readonly actionHandler = inject(ActionHandlerService);
  result = '';

  executeAction(): void {
    this.actionHandler
      .execute('demo.hello-world', { name: 'Claude' })
      .subscribe({
        next: (result) => {
          this.result = result.data || 'No data';
          console.log('Action executed:', result);
        },
        error: (error) => {
          console.error('Action failed:', error);
        }
      });
  }
}

✨ Parameter Metadata System (NEW)

The Parameter Metadata System provides rich, typed parameter definitions with automatic validation and dynamic UI generation capabilities.

Key Benefits

Quick Example

import { ActionParameterBuilder, ParameterBuilders } from '@libs-core/action-registry';

// Define action with rich parameter metadata
export const MY_ACTION_CONFIG = {
  id: 'user.create',
  actionClass: CreateUserAction,
  metadata: {
    name: 'Create User',
    description: 'Creates a new user account',
    type: 'service',
    permissions: ['user.create'],
    tags: ['user', 'management'],
    parameters: [
      ParameterBuilders.email('email')
        .description('User email address')
        .helpText('Will be used for login')
        .build(),

      ActionParameterBuilder.string('username')
        .required()
        .minLength(3)
        .maxLength(20)
        .pattern(/^[a-zA-Z0-9_]+$/, 'Only alphanumeric and underscore allowed')
        .placeholder('john_doe')
        .example('john_doe')
        .build(),

      ActionParameterBuilder.number('age')
        .min(18)
        .max(120)
        .label('Age')
        .helpText('Must be 18 or older')
        .build(),

      ActionParameterBuilder.enum('role', ['admin', 'user', 'guest'])
        .required()
        .defaultValue('user')
        .label('User Role')
        .build()
    ],
    isAsync: true,
    timeout: 10000
  }
};

Dynamic Form Generation

import { DynamicActionFormComponent } from '@libs-core/action-registry';

@Component({
  selector: 'app-create-user',
  standalone: true,
  imports: [DynamicActionFormComponent],
  template: `
    <lib-action-dynamic-form
      [config]="{
        actionId: 'user.create',
        layout: 'vertical',
        showSubmitButton: true,
        submitButtonText: 'Create User'
      }"
      (formSubmit)="onCreateUser($event)"
      (formValidChange)="isFormValid = $event"
    ></lib-action-dynamic-form>
  `
})
export class CreateUserComponent {
  isFormValid = false;

  onCreateUser(params: ActionParams): void {
    // Form is already validated, execute action
    this.actionHandler.execute('user.create', params).subscribe({
      next: (result) => console.log('User created:', result.data),
      error: (error) => console.error('Failed:', error)
    });
  }
}

Parameter Introspection

import { ParameterIntrospectionService } from '@libs-core/action-registry';

@Component({...})
export class ActionExplorerComponent {
  private readonly introspection = inject(ParameterIntrospectionService);

  exploreAction(actionId: string): void {
    // Get parameter analysis
    this.introspection.analyzeParameters(actionId).subscribe(analysis => {
      console.log('Total parameters:', analysis.totalParameters);
      console.log('Required:', analysis.requiredParameters);
      console.log('Optional:', analysis.optionalParameters);
      console.log('Validation complexity:', analysis.validationComplexity);
    });

    // Get required parameters
    this.introspection.getRequiredParameters(actionId).subscribe(params => {
      console.log('Required params:', params);
    });

    // Get default values
    this.introspection.getDefaultValues(actionId).subscribe(defaults => {
      console.log('Defaults:', defaults);
    });
  }
}

Supported Parameter Types

Validator Types

Pre-configured Builders

import { ParameterBuilders } from '@libs-core/action-registry';

// Common parameter patterns
ParameterBuilders.id()          // UUID field
ParameterBuilders.email()       // Email field
ParameterBuilders.url()         // URL field
ParameterBuilders.page()        // Pagination page number
ParameterBuilders.pageSize()    // Pagination page size
ParameterBuilders.search()      // Search query field
ParameterBuilders.dateParam()   // Date field
ParameterBuilders.toggle()      // Boolean switch
ParameterBuilders.select()      // Dropdown selection
ParameterBuilders.textarea()    // Multi-line text

Learn More


📦 Core Components

Services

Contracts and Interfaces

Built-in Actions


📚 Documentation


🏗️ Architecture

Command Pattern Implementation

┌─────────────────┐
│  Client Code    │
└────────┬────────┘
         │ execute()
         ▼
┌─────────────────────────┐
│ ActionHandlerService    │
│ (Invoker)               │
└────────┬────────────────┘
         │ getMetadata()
         ▼
┌─────────────────────────┐
│ ActionRegistryService   │
│ (Registry)              │
└────────┬────────────────┘
         │ getInstance()
         ▼
┌─────────────────────────┐
│ ExecutableAction        │
│ (Command Interface)     │
└────────┬────────────────┘
         │
         ▼
┌─────────────────────────┐
│ Concrete Action         │
│ (Command)               │
└─────────────────────────┘

Key Design Patterns


🔧 Configuration

Action Registration Options

interface ActionRegistrationConfig {
  id: string;                    // Unique action identifier
  actionClass: Type<any>;        // Action class constructor
  metadata: {
    name: string;                // Display name
    description: string;         // Action description
    type: ActionType;            // 'component' | 'service' | 'http' | 'custom'
    permissions: string[];       // Required permissions
    tags: string[];              // Categorization tags
    isAsync: boolean;            // Async execution flag
    timeout: number;             // Execution timeout (ms)
    retryable: boolean;          // Retry on failure
    maxRetries: number;          // Max retry attempts
  };
  overwrite?: boolean;           // Allow overwriting existing
}

Circuit Breaker Configuration

interface CircuitBreakerConfig {
  failureThreshold: number;      // Failures before opening (default: 5)
  resetTimeout: number;          // Recovery timeout (default: 60000ms)
  successThreshold: number;      // Successes to close (default: 2)
  name?: string;                 // Circuit identifier
}

🎯 Common Use Cases

1. Module-to-Module Communication

// Module A exposes action
@PublicAction({
  id: 'process-payment',
  name: 'Process Payment',
  description: 'Processes payment transaction',
  permissions: ['payment.process']
})
export class ProcessPaymentAction implements ExecutableAction<PaymentResult> {
  execute(params, context): Observable<PaymentResult> {
    // Implementation
  }
}

// Module B executes action
this.actionHandler.execute('payment.process-payment', {
  amount: 100,
  currency: 'USD'
}).subscribe(result => {
  console.log('Payment processed:', result.data);
});

2. Permission-based Action Execution

// Get actions available to current user
const userPermissions = ['navigation.basic', 'ui.notification'];
const executableActions = this.actionRegistry
  .getExecutableActions(userPermissions);

console.log('Available actions:', executableActions.map(a => a.name));

3. Batch Action Execution

// Execute multiple actions in parallel
this.actionHandler.executeBatch([
  { actionId: 'core.save-form', parameters: { formData } },
  { actionId: 'core.send-notification', parameters: { message } },
  { actionId: 'core.update-ui', parameters: { theme } }
]).subscribe(result => {
  console.log(`Batch complete: ${result.successful}/${result.total} succeeded`);
});

4. Transactional Execution with Rollback

// Execute actions as transaction with rollback support
this.actionHandler.executeTransaction([
  {
    actionId: 'create-order',
    parameters: { order },
    rollback: () => this.deleteOrder(order.id)
  },
  {
    actionId: 'charge-payment',
    parameters: { payment },
    rollback: () => this.refundPayment(payment.id)
  },
  {
    actionId: 'send-confirmation',
    parameters: { email }
  }
]).subscribe(result => {
  if (result.success) {
    console.log('Transaction completed');
  } else {
    console.log('Transaction failed, rolled back');
  }
});

🧪 Testing

Test Coverage Status ✅

The Action Registry library has comprehensive test coverage:

Testing Actions

import { TestBed } from '@angular/core/testing';
import { lastValueFrom } from 'rxjs';
import { HelloWorldAction } from './hello-world.action';
import { ExecutionContext } from '@libs-core/action-registry';

describe('HelloWorldAction', () => {
  let action: HelloWorldAction;

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [HelloWorldAction]
    });
    action = TestBed.inject(HelloWorldAction);
  });

  it('should execute successfully', async () => {
    const params = { name: 'Test' };
    const context = new ExecutionContext();

    const result = await lastValueFrom(action.execute(params, context));
    expect(result).toBe('Hello Test!');
  });
});

Testing Transaction Workflows

import { lastValueFrom } from 'rxjs';

it('should handle transaction with rollback', async () => {
  const rollbackSpy = vi.fn(() => of(undefined));

  // Use lastValueFrom for transaction observables that emit once after completion
  const result = await lastValueFrom(
    actionHandler.executeTransaction([
      { actionId: 'step-1', parameters: {}, rollback: rollbackSpy },
      { actionId: 'step-2', parameters: {} }, // This fails
    ])
  );

  expect(result.success).toBe(false);
  expect(result.rolledBack).toBe(true);
  expect(rollbackSpy).toHaveBeenCalled();
});

Important Testing Notes

Observable Streams:

Event Testing:

Permission Testing:


🔗 Dependencies

Required Libraries

Internal Dependencies


📈 Performance


🛡️ Security


🤝 Contributing

This library is part of the GoGrab Space monorepo. Follow the project's contribution guidelines.


📄 License

MIT License - GoGrab Space Team


🔍 Learn More


📊 Status


Last Updated: 2025-10-15 Version: 1.0.0 Angular: 20.x TypeScript: 5.8 Test Status: Integration tests complete ✅