Examples

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

@libs-core/action-registry - Usage Examples

Practical examples and recipes for common scenarios.


Table of Contents

  1. Basic Action Creation
  2. Module Integration
  3. Permission-based Actions
  4. Batch and Transaction Execution
  5. Circuit Breaker Integration
  6. Custom Action Types
  7. Testing Actions
  8. Advanced Patterns

Basic Action Creation

Simple Service Action

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

@PublicAction({
  id: 'greet-user',
  name: 'Greet User',
  description: 'Displays a greeting message',
  permissions: [],
  tags: ['demo', 'greeting']
})
@Injectable({ providedIn: 'root' })
export class GreetUserAction implements ExecutableAction<string> {
  private readonly logger = inject(LoggerService).child({ source: 'GreetUserAction' });

  execute(params: ActionParams, context: ActionContext): Observable<string> {
    this.logger.info('Greeting user', { params, context: context.toJSON() });

    const name = params['name'] || 'Guest';
    const greeting = `Hello, ${name}! Welcome to GoGrab Space.`;

    return of(greeting);
  }

  validate(params: ActionParams): Observable<boolean> {
    // Optional: validate that name is a non-empty string
    return of(typeof params['name'] === 'string' && params['name'].length > 0);
  }
}

Registering and Executing

import { Component, inject, OnInit } from '@angular/core';
import { ActionRegistryService, ActionHandlerService } from '@libs-core/action-registry';
import { GreetUserAction } from './actions/greet-user.action';

@Component({
  selector: 'app-demo',
  standalone: true,
  template: `
    <h2>Action Registry Demo</h2>
    <button (click)="executeGreeting()">Greet Me</button>
    <p>{{ result }}</p>
  `
})
export class DemoComponent implements OnInit {
  private readonly actionRegistry = inject(ActionRegistryService);
  private readonly actionHandler = inject(ActionHandlerService);

  result = '';

  ngOnInit(): void {
    // Register the action
    this.actionRegistry.register({
      id: 'demo.greet-user',
      actionClass: GreetUserAction,
      metadata: {
        name: 'Greet User',
        description: 'Displays a greeting message',
        type: 'service',
        permissions: [],
        tags: ['demo', 'greeting'],
        isAsync: true,
        timeout: 5000,
        retryable: false,
        maxRetries: 0
      }
    });
  }

  executeGreeting(): void {
    this.actionHandler
      .execute<string>('demo.greet-user', { name: 'Claude' })
      .subscribe({
        next: (result) => {
          if (result.success) {
            this.result = result.data || '';
          } else {
            this.result = `Error: ${result.error?.message}`;
          }
        },
        error: (error) => {
          this.result = `Execution failed: ${error.message}`;
        }
      });
  }
}

Module Integration

Creating a Module Contract

import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
import {
  BaseModuleActionContract,
  ModuleContract,
  ModuleInfo,
  ModuleActionConfig,
  PublicAction
} from '@libs-core/action-registry';

// Define module actions
@PublicAction({
  id: 'calculate-total',
  name: 'Calculate Total',
  description: 'Calculates order total',
  permissions: ['orders.read'],
  tags: ['orders', 'calculation']
})
@Injectable()
export class CalculateTotalAction implements ExecutableAction<number> {
  execute(params: ActionParams, context: ActionContext): Observable<number> {
    const items = params['items'] || [];
    const total = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
    return of(total);
  }
}

// Module Contract
@ModuleContract({
  id: 'orders-module',
  name: 'Orders Module',
  version: '1.0.0',
  description: 'Handles order management',
  author: 'GoGrab Space Team',
  tags: ['orders', 'ecommerce'],
  dependencies: []
})
@Injectable()
export class OrdersModuleContract extends BaseModuleActionContract {
  protected override moduleInfo: ModuleInfo = {
    id: 'orders-module',
    name: 'Orders Module',
    version: '1.0.0',
    description: 'Handles order management',
    tags: ['orders', 'ecommerce'],
    dependencies: []
  };

  override discoverActions(): Observable<ActionDiscoveryResult> {
    const actions: ModuleActionConfig[] = [
      {
        id: 'calculate-total',
        name: 'Calculate Total',
        description: 'Calculates order total',
        type: 'service',
        actionClass: CalculateTotalAction,
        permissions: ['orders.read'],
        tags: ['orders', 'calculation'],
        isPublic: true
      }
    ];

    return of({
      moduleInfo: this.moduleInfo,
      actions,
      success: true,
      errors: [],
      warnings: []
    });
  }
}

Discovering and Registering Module Actions

import { Component, inject, OnInit } from '@angular/core';
import {
  ActionDiscoveryService,
  ActionRegistryService
} from '@libs-core/action-registry';
import { OrdersModuleContract } from './modules/orders/orders-module.contract';

@Component({
  selector: 'app-root',
  standalone: true,
  template: `<router-outlet />`
})
export class AppComponent implements OnInit {
  private readonly discoveryService = inject(ActionDiscoveryService);
  private readonly moduleContract = inject(OrdersModuleContract);

  ngOnInit(): void {
    // Discover and register module actions
    this.discoveryService
      .discoverModuleActions(this.moduleContract)
      .subscribe(result => {
        if (result.success) {
          console.log(`Discovered ${result.actions.length} actions from ${result.moduleInfo.name}`);
          this.discoveryService.registerDiscoveredActions(result);
        } else {
          console.error('Discovery failed:', result.errors);
        }
      });
  }
}

Permission-based Actions

Action with Permission Requirements

@PublicAction({
  id: 'delete-user',
  name: 'Delete User',
  description: 'Permanently deletes a user account',
  permissions: ['users.delete', 'admin.access'],  // Requires BOTH permissions
  tags: ['users', 'admin', 'dangerous']
})
@Injectable({ providedIn: 'root' })
export class DeleteUserAction implements ExecutableAction<boolean> {
  private readonly http = inject(HttpClient);
  private readonly logger = inject(LoggerService).child({ source: 'DeleteUserAction' });

  execute(params: ActionParams, context: ActionContext): Observable<boolean> {
    const userId = params['userId'];

    this.logger.warn('Deleting user', {
      userId,
      executedBy: context.userId,
      context: context.toJSON()
    });

    return this.http.delete<void>(`/api/users/${userId}`).pipe(
      map(() => true),
      catchError(error => {
        this.logger.error('User deletion failed', { error, userId });
        return throwError(() => error);
      })
    );
  }

  validate(params: ActionParams): Observable<boolean> {
    return of(!!params['userId'] && typeof params['userId'] === 'string');
  }
}

Checking Permissions Before Execution

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

@Component({
  selector: 'app-user-admin',
  standalone: true,
  template: `
    @if (canDelete) {
      <button (click)="deleteUser()">Delete User</button>
    }
  `
})
export class UserAdminComponent {
  private readonly actionRegistry = inject(ActionRegistryService);
  private readonly actionHandler = inject(ActionHandlerService);
  private readonly security = inject(SecurityService);
  private readonly permissionChecker = inject(PermissionCheckerService);

  canDelete = false;

  ngOnInit(): void {
    // Check if user can execute delete action
    this.security.getCurrentUser().subscribe(user => {
      const userPermissions = user.permissions || [];
      const executableActions = this.actionRegistry.getExecutableActions(userPermissions);

      this.canDelete = executableActions.some(action => action.id === 'users.delete-user');
    });
  }

  deleteUser(): void {
    this.actionHandler
      .execute('users.delete-user', { userId: '123' })
      .subscribe({
        next: (result) => console.log('User deleted:', result),
        error: (error) => console.error('Deletion failed:', error)
      });
  }
}

Batch and Transaction Execution

Batch Execution (Parallel)

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

@Component({
  selector: 'app-dashboard',
  standalone: true,
  template: `
    <button (click)="loadDashboard()">Load Dashboard</button>
    <p>Loading: {{ loading }}</p>
  `
})
export class DashboardComponent {
  private readonly actionHandler = inject(ActionHandlerService);
  loading = false;

  loadDashboard(): void {
    this.loading = true;

    // Execute multiple data-loading actions in parallel
    this.actionHandler.executeBatch([
      { actionId: 'dashboard.load-user-stats' },
      { actionId: 'dashboard.load-recent-orders' },
      { actionId: 'dashboard.load-notifications' },
      { actionId: 'dashboard.load-analytics' }
    ]).subscribe({
      next: (batchResult) => {
        this.loading = false;
        console.log(`Loaded ${batchResult.successful}/${batchResult.total} dashboard components`);
        console.log(`Completed in ${batchResult.totalExecutionTime}ms`);

        // Process results
        batchResult.results.forEach((result, index) => {
          if (result.success) {
            console.log(`Action ${index} succeeded:`, result.data);
          } else {
            console.error(`Action ${index} failed:`, result.error);
          }
        });
      },
      error: (error) => {
        this.loading = false;
        console.error('Batch execution error:', error);
      }
    });
  }
}

Transactional Execution with Rollback

import { Component, inject } from '@angular/core';
import { ActionHandlerService } from '@libs-core/action-registry';
import { HttpClient } from '@angular/common/http';
import { map } from 'rxjs/operators';

@Component({
  selector: 'app-checkout',
  standalone: true,
  template: `
    <button (click)="processCheckout()">Complete Order</button>
  `
})
export class CheckoutComponent {
  private readonly actionHandler = inject(ActionHandlerService);
  private readonly http = inject(HttpClient);

  processCheckout(): void {
    const orderData = { items: [...], total: 100 };
    const paymentData = { amount: 100, method: 'credit_card' };

    this.actionHandler.executeTransaction([
      {
        actionId: 'orders.create-order',
        parameters: orderData,
        rollback: (result) => {
          console.log('Rolling back order creation');
          const orderId = result.data?.id;
          return this.http.delete(`/api/orders/${orderId}`).pipe(map(() => undefined));
        }
      },
      {
        actionId: 'payments.process-payment',
        parameters: paymentData,
        rollback: (result) => {
          console.log('Rolling back payment');
          const paymentId = result.data?.id;
          return this.http.post(`/api/payments/${paymentId}/refund`, {}).pipe(map(() => undefined));
        }
      },
      {
        actionId: 'inventory.reserve-items',
        parameters: { items: orderData.items },
        rollback: (result) => {
          console.log('Rolling back inventory reservation');
          return this.http.post('/api/inventory/release', { items: orderData.items }).pipe(map(() => undefined));
        }
      },
      {
        actionId: 'notifications.send-confirmation',
        parameters: { email: '[email protected]' }
        // No rollback needed for notifications
      }
    ]).subscribe({
      next: (transactionResult) => {
        if (transactionResult.success) {
          console.log('✅ Order completed successfully!');
        } else {
          console.log('❌ Order failed and was rolled back');
          console.log('Executed:', transactionResult.executedCount);
          console.log('Rolled back:', transactionResult.rolledBack);
        }
      }
    });
  }
}

Circuit Breaker Integration

Protected External API Call

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

@PublicAction({
  id: 'fetch-weather',
  name: 'Fetch Weather',
  description: 'Fetches weather data from external API',
  permissions: [],
  tags: ['external', 'api', 'weather']
})
@Injectable({ providedIn: 'root' })
export class FetchWeatherAction implements ExecutableAction<any> {
  private readonly http = inject(HttpClient);
  private readonly circuitBreaker = inject(CircuitBreakerService);

  execute(params: ActionParams, context: ActionContext): Observable<any> {
    const city = params['city'] || 'London';

    // Execute with circuit breaker protection
    return this.circuitBreaker.execute(
      'external-weather-api',
      () => this.http.get(`https://api.weather.com/data?city=${city}`),
      {
        failureThreshold: 3,        // Open after 3 failures
        resetTimeout: 30000,        // Try recovery after 30s
        successThreshold: 2,        // Close after 2 successes
        name: 'Weather API'
      }
    );
  }
}

Monitoring Circuit Breaker State

import { Component, inject, OnInit, computed } from '@angular/core';
import { CircuitBreakerService } from '@libs-core/action-registry';
import { interval } from 'rxjs';

@Component({
  selector: 'app-circuit-monitor',
  standalone: true,
  template: `
    <h3>Circuit Breaker Monitoring</h3>

    @for (entry of metrics(); track entry.id) {
      <div class="circuit-card">
        <h4>{{ entry.id }}</h4>
        <p>State: <span [class]="'state-' + entry.metrics.state">{{ entry.metrics.state }}</span></p>
        <p>Success Rate: {{ entry.metrics.successRate.toFixed(2) }}%</p>
        <p>Total Calls: {{ entry.metrics.totalCalls }}</p>
        <p>Rejected: {{ entry.metrics.rejectedCalls }}</p>
        <p>Last Change: {{ entry.metrics.lastStateChangeAt | date:'short' }}</p>

        @if (entry.metrics.state === 'open') {
          <button (click)="resetCircuit(entry.id)">Reset Circuit</button>
        }
      </div>
    }
  `,
  styles: [`
    .state-closed { color: green; }
    .state-open { color: red; }
    .state-half-open { color: orange; }
  `]
})
export class CircuitMonitorComponent implements OnInit {
  private readonly circuitBreaker = inject(CircuitBreakerService);

  metrics = computed(() => {
    const allMetrics = this.circuitBreaker.getAllMetrics();
    return Object.entries(allMetrics).map(([id, metrics]) => ({ id, metrics }));
  });

  ngOnInit(): void {
    // Refresh metrics every 5 seconds
    interval(5000).subscribe(() => {
      // Metrics auto-update via signals
    });
  }

  resetCircuit(circuitId: string): void {
    this.circuitBreaker.reset(circuitId);
  }
}

Custom Action Types

Component Action with Dynamic UI

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

@PublicAction({
  id: 'show-modal',
  name: 'Show Modal',
  description: 'Displays a modal dialog',
  permissions: [],
  tags: ['ui', 'modal']
})
@Component({
  selector: 'app-modal',
  standalone: true,
  template: `
    <div class="modal">
      <h2>{{ title }}</h2>
      <p>{{ message }}</p>
      <button (click)="close()">Close</button>
    </div>
  `
})
export class ModalComponent implements ExecutableAction<boolean> {
  @Input() title = '';
  @Input() message = '';

  private onCloseCallback?: () => void;

  execute(params: ActionParams, context: ActionContext): Observable<boolean> {
    this.title = params['title'] || 'Modal';
    this.message = params['message'] || '';

    return new Observable(subscriber => {
      this.onCloseCallback = () => {
        subscriber.next(true);
        subscriber.complete();
      };
    });
  }

  close(): void {
    this.onCloseCallback?.();
  }
}

HTTP Action with Custom Headers

@PublicAction({
  id: 'api-call',
  name: 'API Call',
  description: 'Makes authenticated API call',
  permissions: ['api.access'],
  tags: ['api', 'http']
})
@Injectable({ providedIn: 'root' })
export class ApiCallAction implements ExecutableAction<any> {
  private readonly http = inject(HttpClient);
  private readonly security = inject(SecurityService);

  execute(params: ActionParams, context: ActionContext): Observable<any> {
    const endpoint = params['endpoint'];
    const method = params['method'] || 'GET';
    const body = params['body'];

    // Get auth token
    return this.security.getAuthToken().pipe(
      switchMap(token => {
        const headers = {
          'Authorization': `Bearer ${token}`,
          'X-Execution-Id': context.executionId,
          'Content-Type': 'application/json'
        };

        switch (method.toUpperCase()) {
          case 'GET':
            return this.http.get(endpoint, { headers });
          case 'POST':
            return this.http.post(endpoint, body, { headers });
          case 'PUT':
            return this.http.put(endpoint, body, { headers });
          case 'DELETE':
            return this.http.delete(endpoint, { headers });
          default:
            return throwError(() => new Error(`Unsupported HTTP method: ${method}`));
        }
      })
    );
  }
}

Testing Actions

Unit Testing an Action

import { TestBed } from '@angular/core/testing';
import { GreetUserAction } from './greet-user.action';
import { ExecutionContext } from '@libs-core/action-registry';
import { LoggerService } from '@libs-core/log-system';

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

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        GreetUserAction,
        {
          provide: LoggerService,
          useValue: {
            child: () => ({
              info: jest.fn(),
              error: jest.fn()
            })
          }
        }
      ]
    });

    action = TestBed.inject(GreetUserAction);
  });

  it('should greet user with provided name', (done) => {
    const params = { name: 'Alice' };
    const context = new ExecutionContext();

    action.execute(params, context).subscribe({
      next: (result) => {
        expect(result).toBe('Hello, Alice! Welcome to GoGrab Space.');
        done();
      }
    });
  });

  it('should validate params correctly', (done) => {
    action.validate({ name: 'Bob' }).subscribe(isValid => {
      expect(isValid).toBe(true);
      done();
    });

    action.validate({ name: '' }).subscribe(isValid => {
      expect(isValid).toBe(false);
      done();
    });
  });
});

Integration Testing with ActionHandler

import { TestBed } from '@angular/core/testing';
import {
  ActionRegistryService,
  ActionHandlerService
} from '@libs-core/action-registry';
import { GreetUserAction } from './greet-user.action';

describe('Action Registry Integration', () => {
  let registry: ActionRegistryService;
  let handler: ActionHandlerService;

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        ActionRegistryService,
        ActionHandlerService,
        GreetUserAction
      ]
    });

    registry = TestBed.inject(ActionRegistryService);
    handler = TestBed.inject(ActionHandlerService);

    // Register action
    registry.register({
      id: 'test.greet',
      actionClass: GreetUserAction,
      metadata: {
        name: 'Greet',
        description: 'Test action',
        type: 'service',
        permissions: [],
        tags: ['test'],
        isAsync: true,
        timeout: 5000,
        retryable: false,
        maxRetries: 0
      }
    });
  });

  it('should execute registered action', (done) => {
    handler.execute('test.greet', { name: 'Test' }).subscribe({
      next: (result) => {
        expect(result.success).toBe(true);
        expect(result.data).toContain('Test');
        done();
      }
    });
  });

  it('should fail for non-existent action', (done) => {
    handler.execute('test.nonexistent', {}).subscribe({
      next: (result) => {
        expect(result.success).toBe(false);
        expect(result.error).toBeDefined();
        done();
      }
    });
  });
});

Advanced Patterns

Chaining Actions with RxJS

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

@Component({
  selector: 'app-workflow',
  standalone: true,
  template: `<button (click)="runWorkflow()">Run Workflow</button>`
})
export class WorkflowComponent {
  private readonly actionHandler = inject(ActionHandlerService);

  runWorkflow(): void {
    this.actionHandler.execute('step1.validate-input', { data: '...' })
      .pipe(
        tap(result => console.log('Step 1 complete:', result)),
        switchMap(result => {
          // Use result from step 1 as input for step 2
          return this.actionHandler.execute('step2.process-data', { input: result.data });
        }),
        tap(result => console.log('Step 2 complete:', result)),
        switchMap(result => {
          return this.actionHandler.execute('step3.save-result', { processed: result.data });
        })
      )
      .subscribe({
        next: (finalResult) => console.log('Workflow complete:', finalResult),
        error: (error) => console.error('Workflow failed:', error)
      });
  }
}

Dynamic Action Registration from Configuration

import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { ActionRegistryService } from '@libs-core/action-registry';
import { switchMap } from 'rxjs/operators';

interface ActionConfig {
  id: string;
  name: string;
  type: 'http';
  endpoint: string;
  method: string;
  permissions: string[];
}

@Injectable({ providedIn: 'root' })
export class DynamicActionLoader {
  private readonly http = inject(HttpClient);
  private readonly actionRegistry = inject(ActionRegistryService);

  loadActionsFromServer(): void {
    this.http.get<ActionConfig[]>('/api/actions/config')
      .subscribe(configs => {
        configs.forEach(config => {
          this.registerHttpAction(config);
        });
      });
  }

  private registerHttpAction(config: ActionConfig): void {
    // Create dynamic HTTP action class
    class DynamicHttpAction implements ExecutableAction<any> {
      private readonly http = inject(HttpClient);

      execute(params: ActionParams, context: ActionContext): Observable<any> {
        const url = config.endpoint;
        const method = config.method.toUpperCase();

        switch (method) {
          case 'GET':
            return this.http.get(url);
          case 'POST':
            return this.http.post(url, params);
          default:
            return throwError(() => new Error(`Unsupported method: ${method}`));
        }
      }
    }

    this.actionRegistry.register({
      id: config.id,
      actionClass: DynamicHttpAction,
      metadata: {
        name: config.name,
        description: `Dynamic HTTP action for ${config.endpoint}`,
        type: 'http',
        permissions: config.permissions,
        tags: ['dynamic', 'http'],
        isAsync: true,
        timeout: 30000,
        retryable: true,
        maxRetries: 3
      }
    });
  }
}

Examples Version: 1.0.0 Last Updated: 2025-10-14 Status: Complete

For more information, see: