Appearance
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 fieldsmetadata: Extended informationconfiguration: Settings and preferencesvalidation: Data validation rules
Workflow Extensions
workflow: Process automationapproval: Approval workflowsnotification: Alert systemsintegration: External system connections
UI Extensions
widget: Dashboard widgetssidebar: Sidebar componentstoolbar: Toolbar extensionsmodal: Custom modals
Business Logic
calculator: Computed fieldsvalidator: Business rule validationtransformer: Data transformationscheduler: 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