Skip to content

Add-ons Layer

Overview

The Add-ons Layer in Oktuple provides a flexible and extensible system for adding custom functionality to the core project management platform. Add-ons can extend data models, add new features, integrate with external systems, and customize workflows without modifying the core system.

Addon Architecture

Core Concept

Addons are modular extensions that can be attached to ENodes (project nodes) to provide additional functionality. Each addon is self-contained with its own metadata, configuration, and lifecycle management.

Key Characteristics

  • Modular: Independent functionality units
  • Configurable: Flexible metadata and settings
  • Hierarchical: Support for nested addon structures
  • Active/Inactive: Runtime enable/disable capability
  • Type-based: Categorized by functionality
  • Metadata-driven: Flexible data storage

Addon Structure

Core Attributes

typescript
interface Addon {
  id: string;                    // Unique identifier
  type?: string;                 // Addon classification
  owner_id?: string;             // Creator/owner
  title: string;                 // Display name
  parent_id?: number;            // Parent addon (for nesting)
  metadata?: any;                // Flexible configuration data
  mpath?: string;                // Materialized path for hierarchy
  active: boolean;               // Enabled status
  created_at: Date;              // Creation timestamp
  updated_at: Date;              // Last update
  enode_id: string;              // Associated ENode
  is_abstract: boolean;          // Abstract addon flag
  old_id?: string;               // Legacy system ID
  deleted_at?: Date;             // Soft deletion
}

Addon Types

Data Extensions

  • custom_field: Additional data fields
  • metadata: Extended information
  • configuration: Settings and preferences
  • validation: Data validation rules

Workflow Extensions

  • workflow: Process automation
  • approval: Approval workflows
  • notification: Alert systems
  • integration: External system connections

UI Extensions

  • widget: Dashboard widgets
  • sidebar: Sidebar components
  • toolbar: Toolbar extensions
  • modal: Custom modals

Business Logic

  • calculator: Computed fields
  • validator: Business rule validation
  • transformer: Data transformation
  • scheduler: Time-based operations

Addon Management

CRUD Operations

Create Addon

typescript
export class AddonService {
  async createAddon(data: CreateAddonData): Promise<Addon> {
    // Validate addon data
    await this.validateAddonData(data);
    
    // Generate materialized path if parent specified
    const mpath = data.parent_id ? 
      await this.generateMaterializedPath(data.parent_id) : 
      undefined;
    
    // Create addon
    const addon = await this.prisma.addon.create({
      data: {
        ...data,
        mpath,
        active: true,
        created_at: new Date(),
        updated_at: new Date()
      }
    });
    
    // Initialize addon-specific logic
    await this.initializeAddon(addon);
    
    // Emit creation event
    await this.eventBus.emit('addon.created', { addon });
    
    return addon;
  }
  
  private async validateAddonData(data: CreateAddonData): Promise<void> {
    // Check if ENode exists
    const enode = await this.prisma.enode.findUnique({
      where: { id: data.enode_id }
    });
    
    if (!enode) {
      throw new Error('ENode not found');
    }
    
    // Validate addon type
    if (data.type && !this.isValidAddonType(data.type)) {
      throw new Error(`Invalid addon type: ${data.type}`);
    }
    
    // Check for duplicate addons
    const existing = await this.prisma.addon.findFirst({
      where: {
        enode_id: data.enode_id,
        type: data.type,
        title: data.title,
        deleted_at: null
      }
    });
    
    if (existing) {
      throw new Error('Addon with same type and title already exists');
    }
  }
}

Update Addon

typescript
async updateAddon(id: string, data: UpdateAddonData): Promise<Addon> {
  // Validate update data
  await this.validateUpdateData(id, data);
  
  // Update addon
  const addon = await this.prisma.addon.update({
    where: { id },
    data: {
      ...data,
      updated_at: new Date()
    }
  });
  
  // Update materialized paths if parent changed
  if (data.parent_id) {
    await this.updateDescendantPaths(id);
  }
  
  // Emit update event
  await this.eventBus.emit('addon.updated', { addon });
  
  return addon;
}

async deleteAddon(id: string, hardDelete: boolean = false): Promise<void> {
  if (hardDelete) {
    // Hard delete - remove all data
    await this.prisma.$transaction(async (tx) => {
      // Delete all descendants
      const descendants = await this.getDescendants(id);
      for (const descendant of descendants) {
        await tx.addon.delete({ where: { id: descendant.id } });
      }
      
      // Delete addon
      await tx.addon.delete({ where: { id } });
    });
  } else {
    // Soft delete - mark as deleted
    await this.prisma.addon.update({
      where: { id },
      data: { 
        deleted_at: new Date(),
        active: false
      }
    });
  }
  
  // Emit deletion event
  await this.eventBus.emit('addon.deleted', { addonId: id });
}

Addon Lifecycle

Activation/Deactivation

typescript
async activateAddon(addonId: string): Promise<void> {
  const addon = await this.prisma.addon.update({
    where: { id: addonId },
    data: { 
      active: true,
      updated_at: new Date()
    }
  });
  
  // Initialize addon if needed
  await this.initializeAddon(addon);
  
  // Emit activation event
  await this.eventBus.emit('addon.activated', { addon });
}

async deactivateAddon(addonId: string): Promise<void> {
  const addon = await this.prisma.addon.update({
    where: { id: addonId },
    data: { 
      active: false,
      updated_at: new Date()
    }
  });
  
  // Cleanup addon resources
  await this.cleanupAddon(addon);
  
  // Emit deactivation event
  await this.eventBus.emit('addon.deactivated', { addon });
}

Addon Hierarchy

Nested Structure

Parent-Child Relationships

typescript
// Get addon children
async getChildren(parentId: number): Promise<Addon[]> {
  return await this.prisma.addon.findMany({
    where: { 
      parent_id: parentId,
      deleted_at: null,
      active: true
    },
    orderBy: { created_at: 'asc' }
  });
}

// Get addon descendants
async getDescendants(addonId: string): Promise<Addon[]> {
  const descendants: Addon[] = [];
  const children = await this.getChildren(parseInt(addonId));
  
  for (const child of children) {
    descendants.push(child);
    descendants.push(...(await this.getDescendants(child.id)));
  }
  
  return descendants;
}

// Get addon ancestors
async getAncestors(addonId: string): Promise<Addon[]> {
  const ancestors: Addon[] = [];
  let current = await this.prisma.addon.findUnique({
    where: { id: addonId }
  });
  
  while (current?.parent_id) {
    current = await this.prisma.addon.findUnique({
      where: { id: current.parent_id.toString() }
    });
    if (current) ancestors.unshift(current);
  }
  
  return ancestors;
}

Materialized Paths

Path Generation

typescript
private async generateMaterializedPath(parentId: number): Promise<string> {
  const parent = await this.prisma.addon.findUnique({
    where: { id: parentId }
  });
  
  if (!parent) {
    throw new Error('Parent addon not found');
  }
  
  const parentPath = parent.mpath || '';
  return `${parentPath}${parentId}.`;
}

private async updateDescendantPaths(addonId: string): Promise<void> {
  const descendants = await this.getDescendants(addonId);
  const addon = await this.prisma.addon.findUnique({
    where: { id: addonId }
  });
  
  if (!addon) return;
  
  const newPath = addon.mpath || '';
  
  for (const descendant of descendants) {
    const relativePath = descendant.mpath?.replace(addon.mpath || '', '') || '';
    const updatedPath = `${newPath}${relativePath}`;
    
    await this.prisma.addon.update({
      where: { id: descendant.id },
      data: { mpath: updatedPath }
    });
  }
}

Addon Types Implementation

Custom Fields

Field Definition

typescript
interface CustomFieldAddon {
  type: 'custom_field';
  metadata: {
    field_name: string;
    field_type: 'text' | 'number' | 'date' | 'boolean' | 'select' | 'multi_select';
    label: string;
    required: boolean;
    default_value?: any;
    validation_rules?: ValidationRule[];
    options?: string[]; // For select/multi_select
  };
}

export class CustomFieldService {
  async createCustomField(enodeId: string, fieldData: CustomFieldAddon['metadata']): Promise<Addon> {
    return await this.addonService.createAddon({
      type: 'custom_field',
      title: fieldData.label,
      enode_id: enodeId,
      metadata: fieldData
    });
  }
  
  async getCustomFields(enodeId: string): Promise<CustomFieldAddon[]> {
    const addons = await this.addonService.getAddonsByType(enodeId, 'custom_field');
    return addons.map(addon => ({
      ...addon,
      metadata: addon.metadata as CustomFieldAddon['metadata']
    }));
  }
}

Workflow Addons

Workflow Definition

typescript
interface WorkflowAddon {
  type: 'workflow';
  metadata: {
    workflow_name: string;
    steps: WorkflowStep[];
    triggers: WorkflowTrigger[];
    conditions: WorkflowCondition[];
  };
}

interface WorkflowStep {
  id: string;
  name: string;
  type: 'action' | 'approval' | 'notification' | 'condition';
  action?: string;
  assignee?: string;
  timeout?: number;
  next_steps: string[];
}

export class WorkflowService {
  async executeWorkflow(workflowId: string, context: any): Promise<void> {
    const addon = await this.addonService.getAddon(workflowId);
    if (addon.type !== 'workflow') {
      throw new Error('Invalid workflow addon');
    }
    
    const workflow = addon.metadata as WorkflowAddon['metadata'];
    await this.executeWorkflowSteps(workflow.steps, context);
  }
  
  private async executeWorkflowSteps(steps: WorkflowStep[], context: any): Promise<void> {
    for (const step of steps) {
      await this.executeStep(step, context);
    }
  }
}

Integration Addons

External System Integration

typescript
interface IntegrationAddon {
  type: 'integration';
  metadata: {
    system_name: string;
    api_endpoint: string;
    authentication: AuthConfig;
    mappings: FieldMapping[];
    webhooks: WebhookConfig[];
  };
}

export class IntegrationService {
  async syncWithExternalSystem(integrationId: string, data: any): Promise<void> {
    const addon = await this.addonService.getAddon(integrationId);
    if (addon.type !== 'integration') {
      throw new Error('Invalid integration addon');
    }
    
    const integration = addon.metadata as IntegrationAddon['metadata'];
    
    // Authenticate with external system
    const token = await this.authenticate(integration.authentication);
    
    // Map data fields
    const mappedData = this.mapFields(data, integration.mappings);
    
    // Send data to external system
    await this.sendToExternalSystem(integration.api_endpoint, mappedData, token);
  }
}

Addon Metadata Management

Flexible Data Storage

Metadata Structure

typescript
// Example metadata for different addon types
const customFieldMetadata = {
  field_name: 'priority',
  field_type: 'select',
  label: 'Priority',
  required: true,
  options: ['Low', 'Medium', 'High', 'Critical'],
  validation_rules: [
    { type: 'required', message: 'Priority is required' }
  ]
};

const workflowMetadata = {
  workflow_name: 'Bug Triage',
  steps: [
    {
      id: '1',
      name: 'Initial Review',
      type: 'action',
      action: 'assign_reviewer',
      next_steps: ['2']
    },
    {
      id: '2',
      name: 'Technical Review',
      type: 'approval',
      assignee: 'tech_lead',
      next_steps: ['3']
    }
  ]
};

const integrationMetadata = {
  system_name: 'Jira',
  api_endpoint: 'https://company.atlassian.net/rest/api/2',
  authentication: {
    type: 'basic',
    username: '${JIRA_USERNAME}',
    password: '${JIRA_API_TOKEN}'
  },
  mappings: [
    { source: 'title', target: 'summary' },
    { source: 'description', target: 'description' }
  ]
};

Metadata Validation

Schema Validation

typescript
import { z } from 'zod';

const CustomFieldSchema = z.object({
  field_name: z.string().min(1),
  field_type: z.enum(['text', 'number', 'date', 'boolean', 'select', 'multi_select']),
  label: z.string().min(1),
  required: z.boolean(),
  default_value: z.any().optional(),
  validation_rules: z.array(z.any()).optional(),
  options: z.array(z.string()).optional()
});

const WorkflowSchema = z.object({
  workflow_name: z.string().min(1),
  steps: z.array(z.object({
    id: z.string(),
    name: z.string(),
    type: z.enum(['action', 'approval', 'notification', 'condition']),
    action: z.string().optional(),
    assignee: z.string().optional(),
    timeout: z.number().optional(),
    next_steps: z.array(z.string())
  })),
  triggers: z.array(z.any()).optional(),
  conditions: z.array(z.any()).optional()
});

export class MetadataValidator {
  async validateMetadata(type: string, metadata: any): Promise<boolean> {
    try {
      switch (type) {
        case 'custom_field':
          CustomFieldSchema.parse(metadata);
          break;
        case 'workflow':
          WorkflowSchema.parse(metadata);
          break;
        case 'integration':
          // Integration validation logic
          break;
        default:
          // Generic metadata validation
          break;
      }
      return true;
    } catch (error) {
      throw new Error(`Invalid metadata for addon type ${type}: ${error.message}`);
    }
  }
}

Addon Events and Hooks

Event System

Addon Events

typescript
export enum AddonEventType {
  CREATED = 'addon.created',
  UPDATED = 'addon.updated',
  DELETED = 'addon.deleted',
  ACTIVATED = 'addon.activated',
  DEACTIVATED = 'addon.deactivated',
  EXECUTED = 'addon.executed'
}

export interface AddonEvent {
  type: AddonEventType;
  addon: Addon;
  timestamp: Date;
  context?: any;
  userId?: string;
}

export class AddonEventService {
  async emitEvent(event: AddonEvent): Promise<void> {
    // Emit to event bus
    await this.eventBus.emit(event.type, event);
    
    // Send WebSocket notifications
    await this.websocketService.notifyAddonEvent(event);
    
    // Execute addon hooks
    await this.executeAddonHooks(event);
  }
  
  private async executeAddonHooks(event: AddonEvent): Promise<void> {
    const hooks = await this.getAddonHooks(event.addon.id, event.type);
    
    for (const hook of hooks) {
      try {
        await hook.execute(event);
      } catch (error) {
        console.error(`Error executing addon hook: ${error.message}`);
      }
    }
  }
}

Hook System

Addon Hooks

typescript
interface AddonHook {
  id: string;
  addon_id: string;
  event_type: AddonEventType;
  hook_type: 'pre' | 'post' | 'around';
  script: string;
  active: boolean;
}

export class AddonHookService {
  async executeHook(hook: AddonHook, event: AddonEvent): Promise<void> {
    if (!hook.active) return;
    
    try {
      // Execute hook script
      const result = await this.executeScript(hook.script, event);
      
      // Log hook execution
      await this.logHookExecution(hook, event, result);
      
    } catch (error) {
      // Log hook error
      await this.logHookError(hook, event, error);
      throw error;
    }
  }
  
  private async executeScript(script: string, context: any): Promise<any> {
    // Script execution logic (sandboxed)
    // This could use a JavaScript engine or custom DSL
    return await this.scriptEngine.execute(script, context);
  }
}

Addon Performance and Caching

Caching Strategy

Addon Cache Management

typescript
export class AddonCacheService {
  private redis: Redis;
  
  async getCachedAddon(id: string): Promise<Addon | null> {
    const cached = await this.redis.get(`addon:${id}`);
    return cached ? JSON.parse(cached) : null;
  }
  
  async cacheAddon(addon: Addon): Promise<void> {
    await this.redis.setex(`addon:${id}`, 3600, JSON.stringify(addon));
  }
  
  async getCachedAddonsByType(enodeId: string, type: string): Promise<Addon[]> {
    const cached = await this.redis.get(`addons:${enodeId}:${type}`);
    return cached ? JSON.parse(cached) : [];
  }
  
  async invalidateAddonCache(addonId: string): Promise<void> {
    await this.redis.del(`addon:${addonId}`);
    
    // Invalidate related caches
    const addon = await this.prisma.addon.findUnique({
      where: { id: addonId }
    });
    
    if (addon) {
      await this.redis.del(`addons:${addon.enode_id}:${addon.type}`);
    }
  }
}

Performance Optimization

Query Optimization

typescript
export class AddonQueryService {
  async getAddonsWithMetadata(enodeId: string, types?: string[]): Promise<Addon[]> {
    const where: any = {
      enode_id: enodeId,
      deleted_at: null,
      active: true
    };
    
    if (types && types.length > 0) {
      where.type = { in: types };
    }
    
    return await this.prisma.addon.findMany({
      where,
      include: {
        // Include related data if needed
      },
      orderBy: [
        { parent_id: 'asc' },
        { created_at: 'asc' }
      ]
    });
  }
  
  async getAddonHierarchy(enodeId: string): Promise<AddonHierarchy> {
    const addons = await this.getAddonsWithMetadata(enodeId);
    return this.buildHierarchy(addons);
  }
  
  private buildHierarchy(addons: Addon[]): AddonHierarchy {
    const hierarchy: AddonHierarchy = {
      root: [],
      children: new Map()
    };
    
    for (const addon of addons) {
      if (addon.parent_id) {
        if (!hierarchy.children.has(addon.parent_id)) {
          hierarchy.children.set(addon.parent_id, []);
        }
        hierarchy.children.get(addon.parent_id)!.push(addon);
      } else {
        hierarchy.root.push(addon);
      }
    }
    
    return hierarchy;
  }
}

Addon Security and Permissions

Access Control

Addon Permissions

typescript
export class AddonPermissionService {
  async checkAddonAccess(addonId: string, userId: string, action: string): Promise<boolean> {
    const addon = await this.prisma.addon.findUnique({
      where: { id: addonId },
      include: {
        enode: {
          include: {
            peoples: {
              where: { id: userId }
            }
          }
        }
      }
    });
    
    if (!addon) return false;
    
    // Check ENode access
    const hasEnodeAccess = await this.permissionService.checkAccess(
      addon.enode_id,
      userId,
      'read'
    );
    
    if (!hasEnodeAccess) return false;
    
    // Check addon-specific permissions
    const addonPermissions = await this.getAddonPermissions(addonId, userId);
    return addonPermissions.includes(action);
  }
  
  async getAddonPermissions(addonId: string, userId: string): Promise<string[]> {
    // Get user's permissions for this addon
    const permissions = await this.prisma.addon_permission.findMany({
      where: {
        addon_id: addonId,
        user_id: userId
      },
      include: {
        permission: true
      }
    });
    
    return permissions.map(p => p.permission.name);
  }
}

Future Enhancements

Planned Features

Advanced Addon System

  • Plugin Marketplace: Centralized addon distribution
  • Version Management: Addon versioning and updates
  • Dependency Management: Addon interdependencies
  • Sandboxing: Secure addon execution environment

Performance Improvements

  • Lazy Loading: Load addons on demand
  • Compilation: Pre-compile addon scripts
  • Parallel Execution: Concurrent addon processing
  • Resource Management: Memory and CPU optimization

Integration Capabilities

  • API Extensions: Custom API endpoints
  • Webhook System: External notifications
  • Event Streaming: Real-time addon events
  • Cross-Service: Inter-service addon communication