BaasixBaasix

Hooks and Custom Endpoints Guide

Note: This guide focuses on practical usage and service access patterns. For a full event matrix and metadata format, see Hooks System.

For complete working examples, check out our sample repository: github.com/baasix/baasix/samples/sample

This guide covers how to use hooks and custom endpoints in BAASIX, including access to services, parameters, and expected return values.

Hooks System

Hook Events

BAASIX provides hooks for various lifecycle events:

  • Read Operations: items.read, items.read.after, items.read.one, items.read.one.after
  • Create Operations: items.create, items.create.after
  • Update Operations: items.update, items.update.after
  • Delete Operations: items.delete, items.delete.after

Hook Registration

export default (hooksService, context) => {
  hooksService.registerHook(
    'collection_name', // Collection to hook into
    'items.create', // Event name
    async (hookData) => {
      // Hook function
      // Your logic here
      return { data: modifiedData };
    },
  );
};

Hook Function Parameters

Hook functions receive a single object parameter containing:

Common Parameters (all hooks)

  • collection - The collection name
  • accountability - User and role information
    • accountability.user - User object with id, email, firstName, etc.
    • accountability.role - Role object with id, name, permissions
  • schema - Collection schema definition
  • db - Drizzle database instance for database operations
  • transaction - Current database transaction (if any)

Event-Specific Parameters

Before Create (items.create)

{
  collection,
    accountability,
    schema,
    db, // Drizzle database instance
    data, // Data being created
    transaction;
}

After Create (items.create.after)

{
  collection,
    accountability,
    schema,
    db, // Drizzle database instance
    data, // Original data that was passed
    document, // Created document with ID and computed fields
    transaction;
}

Before Read (items.read)

{
  collection,
    accountability,
    schema,
    db, // Drizzle database instance
    query, // Query object with filter, fields, sort, etc.
    transaction;
}

After Read (items.read.after)

{
  collection,
    accountability,
    schema,
    db, // Drizzle database instance
    query, // Modified query object
    result, // Query results
    transaction;
}

Before Update (items.update)

{
  collection,
    accountability,
    schema,
    db, // Drizzle database instance
    id, // ID of item being updated
    data, // Update data
    transaction;
}

After Update (items.update.after)

{
  collection,
    accountability,
    schema,
    db, // Drizzle database instance
    id, // ID of updated item
    data, // Original update data
    document, // Updated document
    transaction;
}

Before Delete (items.delete)

{
  collection,
    accountability,
    schema,
    db, // Drizzle database instance
    id, // ID of item to delete
    transaction;
}

After Delete (items.delete.after)

{
  collection,
    accountability,
    schema,
    db, // Drizzle database instance
    id, // ID of deleted item
    document, // Deleted document (before deletion)
    transaction;
}

Hook Return Values

Before Hooks

Must return an object containing the modified parameters:

// Before create/update
return {
  data: modifiedData,
  // ... other parameters can be modified
};

// Before read
return {
  query: modifiedQuery,
  // ... other parameters
};

// Before delete
return {
  id: modifiedId,
  // ... other parameters
};

After Hooks

Can return modified data or undefined (no modification):

// After hooks - optional return
return {
  result: modifiedResult, // For read operations
  document: modifiedDocument, // For create/update/delete operations
  // ... other parameters
};

Using Services in Hooks

Access other services within hooks:

import ItemsService from '../../baasix/services/ItemsService';
import FilesService from '../../baasix/services/FilesService';
import NotificationService from '../../baasix/services/NotificationService';

export default (hooksService, context) => {
  hooksService.registerHook(
    'posts',
    'items.create.after',
    async ({ data, document, accountability, schema, transaction }) => {
      // Use other ItemsService instances
      const usersService = new ItemsService('baasix_User', {
        accountability,
        schema,
      });

      const user = await usersService.readOne(accountability.user.id);

      // Use NotificationService
      const notificationService = new NotificationService({
        accountability,
      });

      await notificationService.send({
        to: user.email,
        subject: 'New Post Created',
        body: `Your post "${document.title}" has been created.`,
      });

      return { document };
    },
  );
};

Service Constructors and Parameters

ItemsService

const service = new ItemsService('collection_name', {
  accountability: accountability, // Required for permissions
  schema: schema, // Optional schema object
  tenant: tenantId, // Optional for multi-tenant
});

FilesService

const service = new FilesService({
  accountability: accountability, // Required for permissions
});

NotificationService

const service = new NotificationService({
  accountability: accountability, // Required for user context
});

PermissionService

const service = new PermissionService({
  accountability: accountability, // Required for permission checks
});

Hook Examples

Auto-populate Created/Updated Fields

export default (hooksService, context) => {
  // Before create - add timestamps and user
  hooksService.registerHook('posts', 'items.create', async ({ data, accountability }) => {
    data.created_by = accountability.user.id;
    data.created_at = new Date();
    return { data };
  });

  // Before update - add updated timestamp
  hooksService.registerHook('posts', 'items.update', async ({ data, accountability }) => {
    data.updated_by = accountability.user.id;
    data.updated_at = new Date();
    return { data };
  });
};

Filter Data Based on User Role

export default (hooksService, context) => {
  hooksService.registerHook('posts', 'items.read', async ({ query, accountability }) => {
    // Non-admin users can only see published posts
    if (accountability.role.name !== 'administrator') {
      const existingFilter = query.filter ? JSON.parse(query.filter) : {};
      query.filter = JSON.stringify({
        ...existingFilter,
        published: true,
      });
    }
    return { query };
  });
};

Soft Delete Implementation

export default (hooksService, context) => {
  hooksService.registerHook('posts', 'items.delete', async ({ id, accountability, schema }) => {
    // Instead of deleting, mark as archived
    const postsService = new ItemsService('posts', {
      accountability,
      schema,
    });

    await postsService.updateOne(id, {
      archived: true,
      archived_by: accountability.user.id,
      archived_at: new Date(),
    });

    // Throw error to prevent actual deletion
    throw new Error('Post archived instead of deleted');
  });
};

Custom Endpoints

Endpoint Registration

export default {
  id: 'endpoint-id',
  handler: (app, context) => {
    app.get('/custom-endpoint', async (req, res, next) => {
      try {
        // Your endpoint logic
        res.json({ message: 'Success' });
      } catch (error) {
        next(error);
      }
    });
  },
};

Context Object

The context parameter provides access to:

  • context.db - Drizzle database instance
  • context.permissionService - Permission service
  • context.mailService - Mail service
  • context.storageService - Storage service
  • context.ItemsService - ItemsService class

Request Object Properties

Authentication

  • req.accountability - User and role information (if authenticated)
  • req.accountability.user - Current user object
  • req.accountability.role - Current user's role

Parameters

  • req.params - URL parameters
  • req.query - Query string parameters
  • req.body - Request body data
  • req.headers - Request headers

Using Services in Endpoints

import { APIError } from '../../baasix/utils/errorHandler';
import ItemsService from '../../baasix/services/ItemsService';

export default {
  id: 'user-posts',
  handler: (app, context) => {
    app.get('/user-posts', async (req, res, next) => {
      try {
        // Check authentication
        if (!req.accountability || !req.accountability.user) {
          throw new APIError('Unauthorized', 401);
        }

        const { user } = req.accountability;

        // Use ItemsService to get user's posts
        const postsService = new ItemsService('posts', {
          accountability: req.accountability,
        });

        const posts = await postsService.readByQuery({
          filter: JSON.stringify({ created_by: user.id }),
          sort: ['-created_at'],
        });

        res.json({
          data: posts.data,
          meta: posts.meta,
        });
      } catch (error) {
        next(error);
      }
    });
  },
};

Error Handling

Always use proper error handling in custom endpoints:

import { APIError } from '../../baasix/utils/errorHandler';

// Throw APIError for controlled errors
throw new APIError('Custom error message', 400);

// Use next(error) to pass errors to error handler
try {
  // Your logic
} catch (error) {
  next(error);
}

Advanced Service Usage

Database Transactions

import { getDatabase, ItemsService } from '@baasix/baasix';

export default (hooksService, context) => {
  hooksService.registerHook('orders', 'items.create', async ({ data, accountability, db, transaction }) => {
    // Use the db instance from context for transactions
    // If you need a new transaction:
    const database = getDatabase();

    await database.transaction(async (tx) => {
      // Multiple operations in transaction
      const itemsService = new ItemsService('inventory', {
        accountability,
      });

      // Use SQL expressions for computed updates
      const { sql } = await import('drizzle-orm');

      await itemsService.updateOne(data.product_id, {
        quantity: sql`quantity - 1`,
      });
    });

    return { data };
  });
};

Using Raw SQL Queries

import { getSqlClient } from '@baasix/baasix';

export default (hooksService, context) => {
  hooksService.registerHook('analytics', 'items.read.after', async ({ result, db }) => {
    // Execute raw SQL for complex analytics using tagged template
    const sql = getSqlClient();

    const analytics = await sql`
            SELECT
                DATE_TRUNC('day', "createdAt") as date,
                COUNT(*) as count
            FROM posts
            WHERE "createdAt" >= NOW() - INTERVAL '30 days'
            GROUP BY DATE_TRUNC('day', "createdAt")
            ORDER BY date
        `;

    // Add analytics data to result
    result.analytics = analytics;

    return { result };
  });
};

Extension Directory Structure

Extensions should follow this structure:

extensions/
├── baasix-hook-posts/
│   └── index.js
├── baasix-endpoint-analytics/
│   └── index.js
└── baasix-schedule-reports/
    └── index.js
  • Hook extensions: baasix-hook-{name}/index.js
  • Endpoint extensions: baasix-endpoint-{name}/index.js
  • Schedule extensions: baasix-schedule-{name}/index.js

Best Practices

  1. Always handle errors - Use try/catch and proper error responses
  2. Check permissions - Verify user authentication and authorization
  3. Use transactions - For operations that modify multiple records
  4. Return expected data - Follow the documented return value patterns
  5. Log important events - Use console.log for debugging and audit trails
  6. Validate input data - Check required fields and data types
  7. Use existing services - Leverage BAASIX services rather than direct database access

On this page