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.

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
  • sequelize - Sequelize instance for database operations
  • transaction - Current database transaction (if any)

Event-Specific Parameters

Before Create (items.create)

{
    collection,
    accountability,
    schema,
    sequelize,
    data,           // Data being created
    transaction
}

After Create (items.create.after)

{
    collection,
    accountability,
    schema,
    sequelize,
    data,           // Original data that was passed
    document,       // Created document with ID and computed fields
    transaction
}

Before Read (items.read)

{
    collection,
    accountability,
    schema,
    sequelize,
    query,          // Query object with filter, fields, sort, etc.
    transaction
}

After Read (items.read.after)

{
    collection,
    accountability,
    schema,
    sequelize,
    query,          // Modified query object
    result,         // Query results
    transaction
}

Before Update (items.update)

{
    collection,
    accountability,
    schema,
    sequelize,
    id,             // ID of item being updated
    data,           // Update data
    transaction
}

After Update (items.update.after)

{
    collection,
    accountability,
    schema,
    sequelize,
    id,             // ID of updated item
    data,           // Original update data
    document,       // Updated document
    transaction
}

Before Delete (items.delete)

{
    collection,
    accountability,
    schema,
    sequelize,
    id,             // ID of item to delete
    transaction
}

After Delete (items.delete.after)

{
    collection,
    accountability,
    schema,
    sequelize,
    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.sequelize - Sequelize database instance
  • Various services and utilities

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

const { getSequelize } = require("../../baasix/utils/database");

export default (hooksService, context) => {
    hooksService.registerHook("orders", "items.create", async ({
        data,
        transaction
    }) => {
        const sequelize = getSequelize();

        // Use existing transaction or create new one
        const t = transaction || await sequelize.transaction();

        try {
            // Multiple operations in transaction
            const itemsService = new ItemsService("inventory", {
                accountability
            });

            await itemsService.updateOne(data.product_id, {
                quantity: sequelize.literal('quantity - 1')
            }, { transaction: t });

            // If we created the transaction, commit it
            if (!transaction) await t.commit();

            return { data };
        } catch (error) {
            if (!transaction) await t.rollback();
            throw error;
        }
    });
};

Using Raw SQL Queries

export default (hooksService, context) => {
    hooksService.registerHook("analytics", "items.read.after", async ({
        result,
        sequelize
    }) => {
        // Execute raw SQL for complex analytics
        const [results] = await sequelize.query(`
            SELECT
                DATE_TRUNC('day', created_at) as date,
                COUNT(*) as count
            FROM posts
            WHERE created_at >= NOW() - INTERVAL '30 days'
            GROUP BY DATE_TRUNC('day', created_at)
            ORDER BY date
        `);

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

        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