BaasixBaasix

Extensions System

← Back to Documentation Home

This guide explains how to create and use extensions for BAASIX to extend its functionality.

Table of Contents

  1. Overview
  2. Extension Types
  3. Extension Structure
  4. Creating Custom Endpoints
  5. Creating Hook Extensions
  6. Creating Schedule Extensions
  7. Accessing Core Services
  8. Extension Configuration
  9. Examples
  10. Best Practices

Overview

BAASIX Extensions provide a powerful way to extend the functionality of the BAASIX API without modifying the core codebase. Extensions allow you to:

  • Create custom API endpoints
  • Add business logic via hooks that execute before/after specific operations
  • Schedule recurring tasks using cron syntax
  • Access and utilize core BAASIX services

Extension Types

BAASIX supports three main types of extensions:

  1. Endpoint Extensions: Create custom API routes and endpoints
  2. Hook Extensions: Execute custom code when specific events occur
  3. Schedule Extensions: Run tasks on a recurring schedule

Extension Structure

Extensions follow a consistent directory structure:

extensions/
  baasix-endpoint-{name}/   # For custom endpoints
    index.js                # Main entry point
    package.json (optional) # For dependencies

  baasix-hook-{name}/       # For custom hooks
    index.js                # Main entry point
    package.json (optional) # For dependencies
  
  baasix-schedule-{name}/   # For scheduled tasks
    index.js                # Main entry point
    package.json (optional) # For dependencies

Each extension must have a unique name to avoid conflicts.

Creating Custom Endpoints

Custom endpoints allow you to add new API routes to the BAASIX API.

Basic Structure

// extensions/baasix-endpoint-my-custom-route/index.js
export default {
  id: 'my-custom-route',
  name: 'My Custom Route',
  version: '1.0.0',
  description: 'Adds a custom API route',
  
  register: ({ app, services, database, env }) => {
    // Define your routes here
    app.get('/custom-endpoint', async (req, res) => {
      try {
        // Your custom logic here
        const data = { message: 'This is a custom endpoint' };
        
        // Send response
        res.json({ data });
      } catch (error) {
        res.status(500).json({ 
          error: { message: error.message } 
        });
      }
    });
  }
};

Authentication and Permissions

Custom endpoints can utilize BAASIX's authentication and permission systems:

export default {
  // ... other properties
  
  register: ({ app, services, database, env }) => {
    const { authenticate, authorize } = services.auth;
    
    app.get('/custom-endpoint', 
      authenticate, // Require authentication
      authorize(['admin']), // Require admin role
      async (req, res) => {
        // Access authenticated user
        const userId = req.accountability.user.id;
        
        // Your custom logic here
        
        res.json({ data: result });
      }
    );
  }
};

Using Services

Access BAASIX core services in your custom endpoints:

export default {
  // ... other properties
  
  register: ({ app, services, database, env }) => {
    const { ItemsService } = services;
    
    app.get('/custom-report', authenticate, async (req, res) => {
      try {
        const accountability = req.accountability;
        const itemsService = new ItemsService('orders', { accountability });
        
        // Get orders
        const orders = await itemsService.readMany({
          filter: { status: 'completed' },
          sort: { createdAt: 'desc' },
          limit: 100
        });
        
        // Process data
        const report = processOrders(orders);
        
        res.json({ data: report });
      } catch (error) {
        res.status(500).json({ 
          error: { message: error.message } 
        });
      }
    });
  }
};

Creating Hook Extensions

Hook extensions allow you to execute custom code before or after specific operations in BAASIX.

Basic Structure

// extensions/baasix-hook-user-activity/index.js
export default {
  id: 'user-activity-log',
  name: 'User Activity Log',
  version: '1.0.0',
  description: 'Logs user activities',
  
  events: [
    {
      collection: 'baasix_User', // Target collection
      action: 'items.update.after', // Event to listen for
      handler: async (data, context) => {
        const { services, database, accountability } = context;
        
        // Log the activity
        await database.models.activity_log.create({
          user_id: accountability.user?.id || null,
          action: 'user.update',
          details: {
            user_id: data.id,
            changes: data._previousDataValues
          }
        });
        
        return data; // Return data to continue the flow
      }
    }
  ]
};

Multiple Event Handlers

You can define multiple event handlers in a single hook extension:

export default {
  id: 'data-validation',
  name: 'Data Validation',
  description: 'Validates data before saving',
  
  events: [
    {
      collection: 'products',
      action: 'items.create.before',
      handler: async (data, context) => {
        // Validate product data
        validateProductData(data);
        return data;
      }
    },
    {
      collection: 'orders',
      action: 'items.create.before',
      handler: async (data, context) => {
        // Validate order data
        validateOrderData(data);
        return data;
      }
    }
  ]
};

Creating Schedule Extensions

Schedule extensions allow you to run tasks on a recurring schedule using cron syntax.

Basic Structure

// extensions/baasix-schedule-daily-cleanup/index.js
export default {
  id: 'daily-cleanup',
  name: 'Daily Cleanup Task',
  version: '1.0.0',
  description: 'Clean up temporary files and old logs daily',
  
  schedules: [
    {
      id: 'cleanup-files',
      schedule: '0 0 * * *', // Run at midnight every day
      handler: async (context) => {
        const { database, services } = context;
        const { Op } = database.Sequelize;
        
        // Delete temporary files older than 7 days
        const cutoffDate = new Date();
        cutoffDate.setDate(cutoffDate.getDate() - 7);
        
        await database.models.baasix_Files.destroy({
          where: {
            temporary: true,
            createdAt: { [Op.lt]: cutoffDate }
          }
        });
        
        // Log the cleanup
        console.log('Temporary files cleanup completed');
      }
    }
  ]
};

Multiple Schedules

You can define multiple scheduled tasks in a single extension:

export default {
  id: 'maintenance-tasks',
  name: 'Maintenance Tasks',
  
  schedules: [
    {
      id: 'daily-cleanup',
      schedule: '0 0 * * *', // Run at midnight every day
      handler: async (context) => {
        // Daily cleanup logic
      }
    },
    {
      id: 'weekly-report',
      schedule: '0 9 * * MON', // Run at 9 AM every Monday
      handler: async (context) => {
        // Generate weekly report
      }
    }
  ]
};

Accessing Core Services

Extension handlers receive a context object that provides access to core BAASIX services:

Available Services

  • services.items: ItemsService for CRUD operations
  • services.auth: Authentication service
  • services.mail: Email sending service
  • services.storage: File storage service
  • services.permissions: Permission management service
  • services.hooks: Hooks management service
  • services.socket: Socket.IO service (if enabled)
  • services.tasks: TasksService for background task management

TasksService Integration

The TasksService helps coordinate background tasks across extensions:

// Import TasksService directly in extensions
import tasksService from "../../baasix/services/TasksService";

// Example: Schedule extension with task coordination
export default {
  id: 'task-processor',
  name: 'Background Task Processor',

  schedules: [
    {
      id: 'process-tasks',
      schedule: '*/5 * * * *', // Every 5 minutes
      handler: async (context) => {
        // Check if a task is already running
        if (await tasksService.isTaskRunning()) {
          console.log("Task already running, skipping...");
          return;
        }

        // Mark task as running
        await tasksService.setTaskRunning(true);

        try {
          // Get cached "Not started" tasks
          const tasks = await tasksService.getNotStartedTasks();

          if (tasks.length === 0) {
            console.log("No tasks to process");
            return;
          }

          // Process first available task
          const task = tasks[0];
          await processBackgroundTask(task, context); // Your custom task processing logic

        } catch (error) {
          console.error("Task processing failed:", error);
        } finally {
          // Always reset running state
          await tasksService.setTaskRunning(false);
        }
      }
    }
  ]
};

For complete TasksService documentation, see TasksService Documentation.

Database Access

Access the Sequelize ORM instance:

const { database } = context;
const { models, sequelize, Sequelize } = database;

// Use models directly
const users = await models.baasix_User.findAll({
  where: { status: 'active' }
});

// Use transactions
await sequelize.transaction(async (transaction) => {
  // Perform database operations in a transaction
});

// Use Sequelize operators
const { Op } = Sequelize;
const recentUsers = await models.baasix_User.findAll({
  where: {
    createdAt: { [Op.gt]: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) }
  }
});

Extension Configuration

Extensions can be configured through environment variables:

export default {
  id: 'email-reports',
  name: 'Email Reports',
  
  register: ({ app, services, env }) => {
    // Access environment variables
    const reportRecipients = env.REPORT_RECIPIENTS?.split(',') || [];
    const reportFrequency = env.REPORT_FREQUENCY || 'daily';
    
    // Use configuration in your extension
  }
};

Examples

Custom Report Endpoint

// extensions/baasix-endpoint-sales-report/index.js
export default {
  id: 'sales-report',
  name: 'Sales Report Endpoint',
  
  register: ({ app, services, database }) => {
    const { authenticate } = services.auth;
    const { ItemsService } = services;
    
    app.get('/sales-report', authenticate, async (req, res) => {
      try {
        const { startDate, endDate } = req.query;
        
        // Validate dates
        if (!startDate || !endDate) {
          return res.status(400).json({
            error: { message: 'startDate and endDate are required' }
          });
        }
        
        // Get sales data
        const itemsService = new ItemsService('orders', { 
          accountability: req.accountability 
        });
        
        const orders = await itemsService.readMany({
          filter: {
            createdAt: { between: [startDate, endDate] },
            status: 'completed'
          }
        });
        
        // Generate report
        const report = {
          totalOrders: orders.length,
          totalRevenue: orders.reduce((sum, order) => sum + order.total, 0),
          averageOrderValue: orders.length > 0 
            ? orders.reduce((sum, order) => sum + order.total, 0) / orders.length
            : 0,
          ordersByDate: groupOrdersByDate(orders)
        };
        
        res.json({ data: report });
      } catch (error) {
        res.status(500).json({
          error: { message: error.message }
        });
      }
    });
    
    function groupOrdersByDate(orders) {
      // Implementation details
    }
  }
};

Automated Data Enrichment Hook

// extensions/baasix-hook-product-enrichment/index.js
export default {
  id: 'product-enrichment',
  name: 'Product Data Enrichment',
  
  events: [
    {
      collection: 'products',
      action: 'items.create.before',
      handler: async (data, context) => {
        // Add metadata based on product name
        if (!data.metadata) data.metadata = {};
        
        // Extract category from name
        const categories = {
          'phone': 'Electronics',
          'laptop': 'Electronics',
          'shirt': 'Clothing',
          'shoes': 'Footwear'
        };
        
        for (const [keyword, category] of Object.entries(categories)) {
          if (data.name.toLowerCase().includes(keyword)) {
            data.metadata.suggestedCategory = category;
            break;
          }
        }
        
        // Set default values
        data.inStock = data.inStock ?? true;
        data.popularity = data.popularity ?? 0;
        
        return data;
      }
    }
  ]
};

Monthly Database Backup Schedule

// extensions/baasix-schedule-db-backup/index.js
export default {
  id: 'db-backup',
  name: 'Database Backup',
  
  schedules: [
    {
      id: 'monthly-backup',
      schedule: '0 0 1 * *', // First day of each month at midnight
      handler: async (context) => {
        const { database, env, services } = context;
        const { mail } = services;
        
        try {
          // Backup logic - depends on your database
          const backupFileName = `backup-${new Date().toISOString().split('T')[0]}.sql`;
          
          // This is just an example - implementation depends on your DB setup
          await executeBackupCommand(database.sequelize, backupFileName);
          
          // Upload backup to storage
          const storageService = new services.StorageService();
          await storageService.upload(
            backupFileName,
            'database-backups',
            { isPrivate: true }
          );
          
          // Send notification email
          await mail.sendMail({
            to: env.ADMIN_EMAIL,
            subject: 'Monthly Database Backup Completed',
            templateName: 'backup-notification',
            context: {
              date: new Date().toLocaleDateString(),
              backupName: backupFileName,
              size: '10MB' // Replace with actual size
            }
          });
          
          console.log(`Backup completed: ${backupFileName}`);
        } catch (error) {
          console.error('Backup failed:', error);
          
          // Send failure notification
          await mail.sendMail({
            to: env.ADMIN_EMAIL,
            subject: 'Monthly Database Backup Failed',
            templateName: 'backup-failure',
            context: {
              date: new Date().toLocaleDateString(),
              error: error.message
            }
          });
        }
      }
    }
  ]
};

async function executeBackupCommand(sequelize, filename) {
  // Implementation depends on your database and system
  // Example for PostgreSQL:
  const { exec } = require('child_process');
  const util = require('util');
  const execPromise = util.promisify(exec);
  
  const { database, host, username, password } = sequelize.config;
  
  await execPromise(
    `PGPASSWORD=${password} pg_dump -U ${username} -h ${host} ${database} > ${filename}`
  );
}

Best Practices

  1. Unique IDs: Ensure each extension has a unique ID to avoid conflicts.

  2. Error Handling: Implement proper error handling in all extensions to prevent disruptions.

  3. Graceful Degradation: Design extensions to fail gracefully and not crash the entire API.

  4. Performance: Be mindful of performance implications, especially for hooks that run frequently.

  5. Service Usage: Use the provided services rather than implementing duplicate functionality.

  6. Versioning: Include version information in your extensions for better management.

  7. Documentation: Document your extensions with clear descriptions of functionality and configuration options.

  8. Isolation: Keep extensions focused on specific functionality rather than creating monolithic extensions.

  9. Security: Be careful with permissions in custom endpoints; always use authentication and authorization where appropriate.

  10. Testing: Create tests for your extensions to ensure they work correctly.

← Back to Documentation Home

On this page