Skip to content

ENode Layer

Overview

The ENode (Project Node) layer is the core architectural component of Oktuple that provides a flexible, hierarchical structure for organizing projects, teams, and work items. ENodes serve as the foundation for project management, enabling complex organizational structures while maintaining simplicity and extensibility.

ENode Architecture

Core Concept

ENode represents any organizational unit in the system, from high-level projects down to individual work items. Each ENode can contain other ENodes, creating a tree-like hierarchy that mirrors real-world project structures.

Key Characteristics

  • Hierarchical: Parent-child relationships for nested structures
  • Flexible: Support for various project types and methodologies
  • Extensible: Custom metadata and addon support
  • Template-driven: Reusable project structures
  • Permission-aware: Role-based access control

ENode Structure

Core Attributes

typescript
interface ENode {
  id: string;                    // Unique identifier
  owner_id?: string;            // Owner/creator
  title: string;                // Human-readable name
  description?: string;         // Detailed description
  domain?: string;              // Business domain
  prefix?: string;              // Short identifier
  type: string;                 // Classification
  parent_id?: string;           // Parent ENode
  template_id?: string;         // Associated template
  template_it_id?: string;      // IT template
  template_ip_id?: string;      // IP template
  list_type?: string;           // List configuration
  materialized_path?: string;   // Hierarchical path
  created_at: Date;             // Creation timestamp
  updated_at: Date;             // Last update
  start_at?: Date;              // Start date
  end_at?: Date;                // End date
  complete_at?: Date;           // Completion date
  deleted_at?: Date;            // Soft deletion
  meta?: any;                   // Flexible metadata
  archived: boolean;            // Archive status
  index: number;                // Display order
  position?: number;            // Position in parent
  old_id?: string;              // Legacy system ID
  has_sprint: boolean;          // Sprint capability
  has_limited: boolean;         // Access control
  inherit_id?: string;          // Inheritance source
  count_project: number;        // Project count
  pids: string[];               // Project IDs
}

ENode Types

Project Types

  • project: Main project container
  • sprint: Time-boxed work period
  • epic: Large work item container
  • story: User story or feature
  • task: Individual work item
  • bug: Defect or issue
  • milestone: Project milestone
  • phase: Project phase

Organizational Types

  • organization: Company or entity
  • team: Work team
  • department: Business unit
  • division: Large organizational unit

Custom Types

  • custom: User-defined types
  • template: Template instances
  • archive: Archived items

Hierarchical Structure

Tree Organization

Root Organization
├── Project A
│   ├── Sprint 1
│   │   ├── Epic: User Authentication
│   │   │   ├── Story: Login Form
│   │   │   ├── Story: Password Reset
│   │   │   └── Story: 2FA Setup
│   │   └── Epic: Dashboard
│   │       ├── Story: Main Dashboard
│   │       └── Story: Widgets
│   └── Sprint 2
│       └── Epic: Reporting
│           ├── Story: Export Data
│           └── Story: Charts
└── Project B
    ├── Phase 1: Planning
    ├── Phase 2: Development
    └── Phase 3: Testing

Materialized Paths

Path Structure

1              # Root level
1.2            # Second level
1.2.3          # Third level
1.2.3.4        # Fourth level

Benefits

  • Fast hierarchical queries
  • Easy level determination
  • Efficient subtree operations
  • Simple path validation

Parent-Child Relationships

typescript
// ENode hierarchy management
export class ENodeService {
  async getChildren(parentId: string): Promise<ENode[]> {
    return await this.prisma.enode.findMany({
      where: { parent_id: parentId },
      orderBy: [{ index: 'asc' }, { position: 'asc' }]
    });
  }
  
  async getAncestors(nodeId: string): Promise<ENode[]> {
    const ancestors: ENode[] = [];
    let current = await this.prisma.enode.findUnique({
      where: { id: nodeId }
    });
    
    while (current?.parent_id) {
      current = await this.prisma.enode.findUnique({
        where: { id: current.parent_id }
      });
      if (current) ancestors.unshift(current);
    }
    
    return ancestors;
  }
  
  async getDescendants(nodeId: string): Promise<ENode[]> {
    const descendants: ENode[] = [];
    const children = await this.getChildren(nodeId);
    
    for (const child of children) {
      descendants.push(child);
      descendants.push(...(await this.getDescendants(child.id)));
    }
    
    return descendants;
  }
}

Template System

Template Integration

Template Types

  • project: Project structure templates
  • workflow: Process templates
  • methodology: Agile, Kanban, Waterfall
  • custom: User-defined templates

Template Application

typescript
export class TemplateService {
  async applyTemplate(enodeId: string, templateId: string): Promise<void> {
    const template = await this.prisma.template.findUnique({
      where: { id: templateId }
    });
    
    if (!template) throw new Error('Template not found');
    
    // Apply template structure
    await this.createTemplateStructure(enodeId, template.rules);
    
    // Apply template metadata
    await this.applyTemplateMetadata(enodeId, template);
    
    // Create template addons
    await this.createTemplateAddons(enodeId, template);
  }
  
  private async createTemplateStructure(enodeId: string, rules: any): Promise<void> {
    if (rules.structure) {
      for (const item of rules.structure) {
        await this.prisma.enode.create({
          data: {
            title: item.title,
            type: item.type,
            parent_id: enodeId,
            template_id: item.template_id,
            meta: item.meta || {}
          }
        });
      }
    }
  }
}

Addon System

Addon Integration

Addon Types

  • custom_field: Extended data fields
  • workflow: Process extensions
  • integration: External system connections
  • reporting: Custom reports
  • notification: Alert systems

Addon Management

typescript
export class AddonService {
  async createAddon(data: CreateAddonData): Promise<Addon> {
    return await this.prisma.addon.create({
      data: {
        id: data.id,
        type: data.type,
        title: data.title,
        enode_id: data.enode_id,
        metadata: data.metadata,
        mpath: data.mpath,
        active: true
      }
    });
  }
  
  async getAddons(enodeId: string): Promise<Addon[]> {
    return await this.prisma.addon.findMany({
      where: { 
        enode_id: enodeId,
        active: true,
        deleted_at: null
      },
      orderBy: { created_at: 'asc' }
    });
  }
  
  async activateAddon(addonId: string): Promise<void> {
    await this.prisma.addon.update({
      where: { id: addonId },
      data: { active: true }
    });
  }
  
  async deactivateAddon(addonId: string): Promise<void> {
    await this.prisma.addon.update({
      where: { id: addonId },
      data: { active: false }
    });
  }
}

Permission System

Access Control

Permission Levels

  • owner: Full control
  • admin: Administrative access
  • member: Standard access
  • viewer: Read-only access
  • guest: Limited access

Permission Inheritance

typescript
export class PermissionService {
  async getUserPermissions(userId: string, enodeId: string): Promise<Permission[]> {
    // Get direct permissions
    const directPermissions = await this.getDirectPermissions(userId, enodeId);
    
    // Get inherited permissions
    const inheritedPermissions = await this.getInheritedPermissions(userId, enodeId);
    
    // Merge and deduplicate
    return this.mergePermissions(directPermissions, inheritedPermissions);
  }
  
  private async getInheritedPermissions(userId: string, enodeId: string): Promise<Permission[]> {
    const ancestors = await this.getAncestors(enodeId);
    const permissions: Permission[] = [];
    
    for (const ancestor of ancestors) {
      const ancestorPermissions = await this.getDirectPermissions(userId, ancestor.id);
      permissions.push(...ancestorPermissions);
    }
    
    return permissions;
  }
}

Role Management

Role Assignment

typescript
export class RoleService {
  async assignRole(userId: string, roleId: string, enodeId: string): Promise<void> {
    await this.prisma.assigned_role.create({
      data: {
        user_id: userId,
        role_id: roleId,
        people_id: null
      }
    });
  }
  
  async getRoles(enodeId: string): Promise<Role[]> {
    return await this.prisma.role.findMany({
      where: { enode_id: enodeId },
      include: {
        permissions: true,
        child_roles: true
      }
    });
  }
}

ENode Operations

CRUD Operations

Create ENode

typescript
export class ENodeService {
  async createENode(data: CreateENodeData): Promise<ENode> {
    // Validate hierarchy
    await this.validateHierarchy(data.parent_id, data.type);
    
    // Generate materialized path
    const materializedPath = await this.generateMaterializedPath(data.parent_id);
    
    // Create ENode
    const enode = await this.prisma.enode.create({
      data: {
        ...data,
        materialized_path: materializedPath,
        index: await this.getNextIndex(data.parent_id)
      }
    });
    
    // Initialize template if provided
    if (data.template_id) {
      await this.templateService.applyTemplate(enode.id, data.template_id);
    }
    
    // Create default addons
    await this.createDefaultAddons(enode.id, data.type);
    
    return enode;
  }
}

Update ENode

typescript
async updateENode(id: string, data: UpdateENodeData): Promise<ENode> {
  // Validate updates
  await this.validateUpdate(id, data);
  
  // Update ENode
  const enode = await this.prisma.enode.update({
    where: { id },
    data: {
      ...data,
      updated_at: new Date()
    }
  });
  
  // Update materialized paths if parent changed
  if (data.parent_id) {
    await this.updateDescendantPaths(id);
  }
  
  // Trigger notifications
  await this.notificationService.notifyENodeUpdate(enode);
  
  return enode;
}

Delete ENode

typescript
async deleteENode(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.enode.delete({ where: { id: descendant.id } });
      }
      
      // Delete ENode
      await tx.enode.delete({ where: { id } });
    });
  } else {
    // Soft delete - mark as deleted
    await this.prisma.enode.update({
      where: { id },
      data: { 
        deleted_at: new Date(),
        archived: true
      }
    });
  }
}

Bulk Operations

Bulk Create

typescript
async bulkCreateENodes(data: CreateENodeData[]): Promise<ENode[]> {
  const enodes: ENode[] = [];
  
  for (const item of data) {
    const enode = await this.createENode(item);
    enodes.push(enode);
  }
  
  return enodes;
}

Bulk Update

typescript
async bulkUpdateENodes(updates: { id: string; data: UpdateENodeData }[]): Promise<void> {
  await this.prisma.$transaction(async (tx) => {
    for (const update of updates) {
      await tx.enode.update({
        where: { id: update.id },
        data: update.data
      });
    }
  });
}

Search and Querying

Query Patterns

Hierarchical Queries

typescript
// Get subtree
async getSubtree(nodeId: string, depth: number = -1): Promise<ENode[]> {
  if (depth === 0) return [];
  
  const children = await this.getChildren(nodeId);
  const subtree: ENode[] = [];
  
  for (const child of children) {
    subtree.push(child);
    if (depth === -1 || depth > 1) {
      subtree.push(...(await this.getSubtree(child.id, depth === -1 ? -1 : depth - 1)));
    }
  }
  
  return subtree;
}

// Get path to root
async getPathToRoot(nodeId: string): Promise<ENode[]> {
  const path: ENode[] = [];
  let current = await this.prisma.enode.findUnique({ where: { id: nodeId } });
  
  while (current) {
    path.unshift(current);
    if (current.parent_id) {
      current = await this.prisma.enode.findUnique({ where: { id: current.parent_id } });
    } else {
      break;
    }
  }
  
  return path;
}

Filtered Queries

typescript
// Get ENodes by type
async getENodesByType(type: string, parentId?: string): Promise<ENode[]> {
  return await this.prisma.enode.findMany({
    where: {
      type,
      parent_id: parentId || null,
      deleted_at: null,
      archived: false
    },
    orderBy: [{ index: 'asc' }, { position: 'asc' }]
  });
}

// Search ENodes
async searchENodes(query: string, filters?: SearchFilters): Promise<ENode[]> {
  const where: any = {
    deleted_at: null,
    archived: false,
    OR: [
      { title: { contains: query, mode: 'insensitive' } },
      { description: { contains: query, mode: 'insensitive' } }
    ]
  };
  
  if (filters?.type) where.type = filters.type;
  if (filters?.parent_id) where.parent_id = filters.parent_id;
  if (filters?.owner_id) where.owner_id = filters.owner_id;
  
  return await this.prisma.enode.findMany({
    where,
    orderBy: [{ title: 'asc' }]
  });
}

Performance Optimization

Indexing Strategy

Database Indexes

sql
-- Primary indexes
CREATE INDEX idx_enode_id ON enode(id);
CREATE INDEX idx_enode_parent_id ON enode(parent_id);
CREATE INDEX idx_enode_type ON enode(type);

-- Performance indexes
CREATE INDEX idx_enode_materialized_path ON enode(materialized_path);
CREATE INDEX idx_enode_owner_id ON enode(owner_id);
CREATE INDEX idx_enode_template_id ON enode(template_id);

-- Composite indexes
CREATE INDEX idx_enode_parent_type ON enode(parent_id, type);
CREATE INDEX idx_enode_type_archived ON enode(type, archived);
CREATE INDEX idx_enode_created_at ON enode(created_at);

-- JSON indexes
CREATE INDEX idx_enode_meta ON enode USING GIN (meta);

Caching Strategy

Redis Caching

typescript
export class ENodeCacheService {
  private redis: Redis;
  
  async getCachedENode(id: string): Promise<ENode | null> {
    const cached = await this.redis.get(`enode:${id}`);
    return cached ? JSON.parse(cached) : null;
  }
  
  async cacheENode(enode: ENode): Promise<void> {
    await this.redis.setex(`enode:${enode.id}`, 3600, JSON.stringify(enode));
  }
  
  async invalidateCache(id: string): Promise<void> {
    await this.redis.del(`enode:${id}`);
  }
  
  async getCachedChildren(parentId: string): Promise<ENode[]> {
    const cached = await this.redis.get(`enode_children:${parentId}`);
    return cached ? JSON.parse(cached) : [];
  }
}

Integration Points

Event System

ENode Events

typescript
export enum ENodeEventType {
  CREATED = 'enode.created',
  UPDATED = 'enode.updated',
  DELETED = 'enode.deleted',
  MOVED = 'enode.moved',
  ARCHIVED = 'enode.archived',
  RESTORED = 'enode.restored'
}

export interface ENodeEvent {
  type: ENodeEventType;
  enode: ENode;
  userId: string;
  timestamp: Date;
  metadata?: any;
}

export class ENodeEventService {
  async emitEvent(event: ENodeEvent): Promise<void> {
    // Emit to event bus
    await this.eventBus.emit(event.type, event);
    
    // Send WebSocket notifications
    await this.websocketService.notifyENodeChange(event);
    
    // Update search indexes
    await this.searchService.indexENode(event.enode);
  }
}

WebSocket Integration

Real-time Updates

typescript
export class ENodeWebSocketService {
  async notifyENodeChange(event: ENodeEvent): Promise<void> {
    const subscribers = await this.getSubscribers(event.enode.id);
    
    for (const subscriber of subscribers) {
      await this.websocketService.sendToUser(subscriber.userId, {
        type: 'enode_update',
        data: event
      });
    }
  }
  
  private async getSubscribers(enodeId: string): Promise<any[]> {
    // Get users with access to this ENode
    return await this.permissionService.getUsersWithAccess(enodeId);
  }
}

Future Enhancements

Planned Features

Advanced Hierarchy

  • Graph Structure: Support for complex relationships
  • Multiple Parents: Items can belong to multiple containers
  • Dynamic Grouping: Automatic categorization
  • Temporal Hierarchy: Time-based organization

Performance Improvements

  • Materialized Views: Pre-computed hierarchies
  • Partitioning: Table partitioning for large datasets
  • Read Replicas: Load distribution
  • Connection Pooling: Advanced connection management

Integration Capabilities

  • External Systems: Third-party tool integration
  • API Extensions: Custom endpoint support
  • Webhook System: External notifications
  • Plugin Architecture: Dynamic functionality loading