Appearance
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 containersprint: Time-boxed work periodepic: Large work item containerstory: User story or featuretask: Individual work itembug: Defect or issuemilestone: Project milestonephase: Project phase
Organizational Types
organization: Company or entityteam: Work teamdepartment: Business unitdivision: Large organizational unit
Custom Types
custom: User-defined typestemplate: Template instancesarchive: 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: TestingMaterialized Paths
Path Structure
1 # Root level
1.2 # Second level
1.2.3 # Third level
1.2.3.4 # Fourth levelBenefits
- 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 templatesworkflow: Process templatesmethodology: Agile, Kanban, Waterfallcustom: 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 fieldsworkflow: Process extensionsintegration: External system connectionsreporting: Custom reportsnotification: 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 controladmin: Administrative accessmember: Standard accessviewer: Read-only accessguest: 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