Module Public Actions Guide

How to correctly implement publicActions in BrowserModuleContract

Module Public Actions Guide

Overview

The editor monorepo uses a simpler ModuleAction interface for public actions, different from the gograb-space monorepo's ExecutableAction classes.

⚠️ Important Distinction

Editor Monorepo (@libs-core/module-base)

// Simple object literals
publicActions?: ModuleAction[];

interface ModuleAction {
  id: string;
  moduleId: string;         // Module that provides this action
  displayName: string;
  description?: string;
  permissions?: string[];
  handler: (params?: Record<string, unknown>) => Observable<unknown>;
}

Gograb-Space Monorepo (@libs-core/action-registry)

// Complex classes with decorators
publicActions?: Type<ExecutableAction>[];

@PublicAction({ id: '...', parameters: [...] })
class MyAction implements ExecutableAction { }

✅ Correct Pattern (Editor Monorepo)

Example: User Management Module

import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
import { BrowserModuleContract, ModuleManifest, ModuleAction } from '@libs-core/module-base';

@Injectable({ providedIn: 'root' })
export class UserManagementModule implements BrowserModuleContract {
  readonly manifest: ModuleManifest = {
    id: 'user-management',
    version: '1.0.0',
    name: 'User Management',
    description: 'User CRUD operations',
    // ... other manifest properties
  };

  // ✅ CORRECT: Simple object literals with ModuleAction interface
  readonly publicActions: ModuleAction[] = [
    {
      id: 'users:create',
      moduleId: 'user-management',  // ⭐ Required!
      displayName: 'Create User',
      description: 'Create a new user in the system',
      permissions: ['users:create'],
      handler: (params?) => this.createUser(params)
    },
    {
      id: 'users:update',
      moduleId: 'user-management',  // ⭐ Required!
      displayName: 'Update User',
      description: 'Update an existing user',
      permissions: ['users:update'],
      handler: (params?) => this.updateUser(params)
    },
    {
      id: 'users:delete',
      moduleId: 'user-management',  // ⭐ Required!
      displayName: 'Delete User',
      description: 'Delete a user from the system',
      permissions: ['users:delete'],
      handler: (params?) => this.deleteUser(params)
    }
  ];

  // Action handler methods
  private createUser(params?: Record<string, unknown>): Observable<unknown> {
    console.log('Creating user:', params);
    // Validate params manually
    if (!params?.name) {
      throw new Error('User name is required');
    }
    // Implementation...
    return of({ success: true, userId: 'usr_123' });
  }

  private updateUser(params?: Record<string, unknown>): Observable<unknown> {
    console.log('Updating user:', params);
    // Implementation...
    return of({ success: true });
  }

  private deleteUser(params?: Record<string, unknown>): Observable<unknown> {
    console.log('Deleting user:', params);
    // Implementation...
    return of({ success: true });
  }

  // Lifecycle methods...
  configure<T>(config: T): Observable<void> { return of(void 0); }
  initialize(): Observable<void> { return of(void 0); }
  start(): Observable<void> { return of(void 0); }
  stop(): Observable<void> { return of(void 0); }
  destroy(): Observable<void> { return of(void 0); }
}

❌ Incorrect Patterns

❌ Missing moduleId

publicActions: ModuleAction[] = [
  {
    id: 'users:create',
    // ❌ Missing moduleId!
    displayName: 'Create User',
    handler: (params?) => of({})
  }
];

❌ Extra Properties

publicActions: ModuleAction[] = [
  {
    id: 'users:create',
    moduleId: 'user-management',
    displayName: 'Create User',
    category: 'crud',        // ❌ Not in ModuleAction interface!
    parameters: [...]        // ❌ Not in ModuleAction interface!
    handler: (params?) => of({})
  }
];

❌ Using ExecutableAction Classes

// ❌ Wrong! This is for gograb-space monorepo
@PublicAction({ id: 'users:create' })
class CreateUserAction implements ExecutableAction { }

publicActions = [CreateUserAction];  // ❌ Won't compile in editor monorepo

🎯 Best Practices

1. Always Include moduleId

{
  id: 'users:create',
  moduleId: 'user-management',  // ⭐ Matches manifest.id
  displayName: 'Create User',
  handler: (params?) => this.createUser(params)
}

2. Keep Handlers Simple

// ✅ Good: Delegate to private methods
handler: (params?) => this.createUser(params)

// ❌ Avoid: Complex inline logic
handler: (params?) => {
  if (!params) return of({ error: 'No params' });
  const user = { name: params.name, email: params.email };
  return this.http.post('/users', user);  // Too complex!
}

3. Return Observables

// ✅ Correct
handler: (params?) => of({ success: true })

// ✅ Also correct
handler: (params?) => this.http.post('/users', params)

// ❌ Wrong
handler: (params?) => { return { success: true }; }  // Not an Observable!

4. Validate Parameters in Handler

private createUser(params?: Record<string, unknown>): Observable<unknown> {
  // ✅ Validate manually
  if (!params?.name || !params?.email) {
    return throwError(() => new Error('Name and email are required'));
  }
  
  // Process...
  return of({ success: true });
}

📋 Complete Module Template

import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
import {
  BrowserModuleContract,
  ModuleManifest,
  ModuleAction,
  ModuleDecorator
} from '@libs-core/module-base';

@ModuleDecorator({
  id: 'my-module',
  version: '1.0.0',
  name: 'My Module',
  description: 'Module description'
})
@Injectable({ providedIn: 'root' })
export class MyModule implements BrowserModuleContract {
  readonly manifest: ModuleManifest = {
    id: 'my-module',
    version: '1.0.0',
    name: 'My Module',
    description: 'Module description',
    author: 'GoGrab Team',
    icon: 'fas fa-cube'
  };

  // ✅ Public actions as instance property
  readonly publicActions: ModuleAction[] = [
    {
      id: 'my-module:action-1',
      moduleId: 'my-module',
      displayName: 'Action 1',
      description: 'First action',
      permissions: ['my-module:read'],
      handler: (params?) => this.handleAction1(params)
    },
    {
      id: 'my-module:action-2',
      moduleId: 'my-module',
      displayName: 'Action 2',
      description: 'Second action',
      permissions: ['my-module:write'],
      handler: (params?) => this.handleAction2(params)
    }
  ];

  readonly routes = [];  // Add routes as needed

  // Action handlers
  private handleAction1(params?: Record<string, unknown>): Observable<unknown> {
    console.log('Action 1 executed:', params);
    return of({ success: true });
  }

  private handleAction2(params?: Record<string, unknown>): Observable<unknown> {
    console.log('Action 2 executed:', params);
    return of({ success: true });
  }

  // Lifecycle methods
  configure<T>(config: T): Observable<void> {
    console.log('Configuring module...');
    return of(void 0);
  }

  initialize(): Observable<void> {
    console.log('Initializing module...');
    return of(void 0);
  }

  start(): Observable<void> {
    console.log('Starting module...');
    return of(void 0);
  }

  stop(): Observable<void> {
    console.log('Stopping module...');
    return of(void 0);
  }

  destroy(): Observable<void> {
    console.log('Destroying module...');
    return of(void 0);
  }
}

🔍 Verification

Test your publicActions:

// In a component or service
const registry = inject(ModuleRegistryService);

// Invoke an action
registry.invokeModuleAction('my-module', 'my-module:action-1', {
  param1: 'value1'
}).subscribe({
  next: (result) => console.log('Action result:', result),
  error: (err) => console.error('Action failed:', err)
});

📚 Related Documentation


Status: ✅ Production Pattern
Last Updated: November 1, 2025
Monorepo: Editor (not gograb-space)