BaasixBaasix

Hooks System

This documentation is verified against the actual API test suite (test/hooks.test.js).

Overview

The Hooks system allows you to execute custom code at specific points during item operations. Hooks enable you to:

  • Modify data before it's saved
  • Add automatic fields (timestamps, user tracking)
  • Filter results based on business rules
  • Prevent operations (soft deletes)
  • Implement custom business logic

Hook Location: Hooks are defined as extensions in extensions/baasix-hook-{name}/index.js


Available Hook Events

Hooks can be registered for the following item operation events:

EventWhen It FiresCan Modify
items.createAfter item is createdCreated data
items.readAfter items are retrievedQuery results
items.updateAfter item is updatedUpdated data
items.deleteBefore item is deletedCan prevent deletion

Hook Context

All hooks receive a context object with information about the request:

{
  collection: 'posts',        // Collection name
  accountability: {          // NOT 'user'! Use 'accountability'
    user: { id: 'user-id' }, // Current user (may be undefined)
    role: { /* role info */ },
    tenant: 'tenant-id'
  },
  db: /* database instance */,
  data: { /* item data */ },
  document: { /* result document */ },
  id: 'item-id',
  query: { /* query parameters */ },
  result: { /* operation result */ },
  transaction: /* db transaction */
}

Important: The context has accountability, not user. Always use context.accountability?.user?.id to access the user ID.


Creating a Hook Extension

Directory Structure

extensions/
  baasix-hook-posts/
    index.js

Basic Hook Structure

export default {
  id: 'posts-hooks',
  handler: async (context) => {
    const { collection, action, accountability, data, document } = context;

    // Your hook logic here

    return data; // or document, depending on the hook
  }
};

Example 1: Auto-Tracking Fields

Add created_by and created_at fields when items are created.

File: extensions/baasix-hook-posts/index.js

export default {
  id: 'posts-create-tracking',
  handler: async (context) => {
    const { collection, action, accountability, document } = context;

    // Only handle posts collection create events
    if (collection === 'posts2' && action === 'items.create') {
      // Add created_by and created_at to the document
      document.created_by = accountability?.user?.id || 'system';
      document.created_at = new Date().toISOString();
    }

    return document;
  }
};

Result: When a post is created, it automatically gets:

{
  "id": 1,
  "title": "Test Post",
  "content": "This is a test",
  "created_by": "user-123",
  "created_at": "2024-01-01T12:00:00.000Z"
}

Example 2: Update Tracking

Add updated_by and updated_at fields when items are updated.

export default {
  id: 'posts-update-tracking',
  handler: async (context) => {
    const { collection, action, accountability, document } = context;

    // Only handle posts collection update events
    if (collection === 'posts2' && action === 'items.update') {
      // Add updated_by and updated_at to the document
      document.updated_by = accountability?.user?.id || 'system';
      document.updated_at = new Date().toISOString();
    }

    return document;
  }
};

Result: When a post is updated:

{
  "id": 1,
  "title": "Updated Post",
  "updated_by": "user-123",
  "updated_at": "2024-01-02T15:30:00.000Z"
}

Example 3: Filter Results (Read Hook)

Show all posts to admins, but only published posts to regular users.

export default {
  id: 'posts-read-filter',
  handler: async (context) => {
    const { collection, action, accountability, result } = context;

    if (collection === 'posts2' && action === 'items.read') {
      const isAdmin = accountability?.role?.name === 'Administrator';

      if (!isAdmin) {
        // Filter out unpublished posts for non-admins
        result.data = result.data.filter(post => post.published === true);
        result.totalCount = result.data.length;
      }
    }

    return result;
  }
};

Result:

  • Admin sees all posts (published and unpublished)
  • Regular users only see published posts

Example 4: Soft Delete (Prevent Deletion)

Archive posts instead of deleting them.

export default {
  id: 'posts-soft-delete',
  handler: async (context) => {
    const { collection, action, accountability, db, id } = context;

    if (collection === 'posts2' && action === 'items.delete') {
      // Instead of deleting, update the post to mark it as archived
      await db.models.posts2.update(
        {
          archived: true,
          archived_by: accountability?.user?.id || 'system',
          archived_at: new Date().toISOString()
        },
        { where: { id } }
      );

      // Prevent the actual deletion by throwing an error
      // (This is intentional - deletion is replaced with archiving)
      throw new Error('Item archived instead of deleted');
    }

    return context;
  }
};

Result:

  • DELETE request returns 500 status
  • Item is not deleted from database
  • Item fields are updated:
    {
      "id": 1,
      "archived": true,
      "archived_by": "user-123",
      "archived_at": "2024-01-03T10:00:00.000Z"
    }

Complete Hook Example

Here's a complete hook that combines all the tracking features:

File: extensions/baasix-hook-posts/index.js

export default {
  id: 'posts-complete-tracking',
  handler: async (context) => {
    const { collection, action, accountability, document, result, db, id } = context;

    // Only handle the 'posts' collection
    if (collection !== 'posts') {
      return document || result || context;
    }

    // Get user ID from accountability
    const userId = accountability?.user?.id || 'system';
    const timestamp = new Date().toISOString();

    switch (action) {
      case 'items.create':
        // Add creation tracking
        document.created_by = userId;
        document.created_at = timestamp;
        return document;

      case 'items.update':
        // Add update tracking
        document.updated_by = userId;
        document.updated_at = timestamp;
        return document;

      case 'items.read':
        // Filter based on user role
        const isAdmin = accountability?.role?.name === 'Administrator';
        if (!isAdmin) {
          result.data = result.data.filter(post => post.published === true);
          result.totalCount = result.data.length;
        }
        return result;

      case 'items.delete':
        // Soft delete (archive)
        await db.models.posts.update(
          {
            archived: true,
            archived_by: userId,
            archived_at: timestamp
          },
          { where: { id } }
        );
        throw new Error('Item archived instead of deleted');

      default:
        return document || result || context;
    }
  }
};

Schema Requirements for Examples

For the above hooks to work, your schema needs these fields:

{
  "collectionName": "posts",
  "schema": {
    "name": "Post",
    "fields": {
      "id": {
        "type": "Integer",
        "primaryKey": true,
        "defaultValue": { "type": "AUTOINCREMENT" }
      },
      "title": { "type": "String", "allowNull": false },
      "content": { "type": "String", "allowNull": false },
      "published": { "type": "Boolean", "default": false },

      // Fields for create tracking
      "created_by": { "type": "String" },
      "created_at": { "type": "DateTime" },

      // Fields for update tracking
      "updated_by": { "type": "String" },
      "updated_at": { "type": "DateTime" },

      // Fields for soft delete
      "archived": { "type": "Boolean", "default": false },
      "archived_by": { "type": "String" },
      "archived_at": { "type": "DateTime" }
    }
  }
}

Hook Execution Flow

Create Flow

1. Client: POST /items/posts { title, content }
2. Hook: items.create fires
3. Hook: Adds created_by, created_at
4. Database: Saves item with tracking fields
5. Response: Returns item with all fields

Update Flow

1. Client: PATCH /items/posts/1 { title: "Updated" }
2. Hook: items.update fires
3. Hook: Adds updated_by, updated_at
4. Database: Updates item with tracking fields
5. Response: Returns updated item

Read Flow

1. Client: GET /items/posts
2. Database: Fetches all posts
3. Hook: items.read fires
4. Hook: Filters based on user role
5. Response: Returns filtered results

Delete Flow (with soft delete)

1. Client: DELETE /items/posts/1
2. Hook: items.delete fires
3. Hook: Updates archived fields
4. Hook: Throws error to prevent deletion
5. Database: Item remains but marked archived
6. Response: 500 error (expected behavior)

Testing Your Hooks

You can test hooks by making API requests:

Test Create Hook

curl -X POST "http://localhost:3000/items/posts" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Test Post",
    "content": "This is a test"
  }'

Check response for created_by and created_at fields.

Test Update Hook

curl -X PATCH "http://localhost:3000/items/posts/1" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Updated Title"
  }'

Check response for updated_by and updated_at fields.

Test Delete Hook (Soft Delete)

curl -X DELETE "http://localhost:3000/items/posts/1" \
  -H "Authorization: Bearer YOUR_TOKEN"

Then verify the item is archived:

curl "http://localhost:3000/items/posts/1" \
  -H "Authorization: Bearer ADMIN_TOKEN"

Should show archived: true, archived_by, and archived_at.


Important Notes

Accountability Object

Always use accountability, never user:

// ❌ WRONG
const userId = context.user.id;

// ✅ CORRECT
const userId = context.accountability?.user?.id;

The accountability object may contain:

  • user - Current user information (may be undefined for anonymous requests)
  • role - User's role information
  • tenant - Tenant ID (in multi-tenant setups)
  • ipaddress - Client IP address

Collection-Specific Hooks

Hooks are collection-specific. Use the collection field to target specific collections:

if (collection === 'posts') {
  // Only run for posts
}

Action Types

The action field indicates which operation triggered the hook:

  • items.create - After creating an item
  • items.read - After reading items
  • items.update - After updating an item
  • items.delete - Before deleting an item

On this page