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
- Basic Action Creation
- Module Integration
- Permission-based Actions
- Batch and Transaction Execution
- Circuit Breaker Integration
- Custom Action Types
- Testing Actions
- 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:
- README.md - Overview and quick start
- API Reference - Complete API documentation
- Design & Architecture - System design and patterns