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:
| Event | When It Fires | Can Modify |
|---|---|---|
items.create | After item is created | Created data |
items.read | After items are retrieved | Query results |
items.update | After item is updated | Updated data |
items.delete | Before item is deleted | Can 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, notuser. Always usecontext.accountability?.user?.idto access the user ID.
Creating a Hook Extension
Directory Structure
extensions/
baasix-hook-posts/
index.jsBasic 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 fieldsUpdate 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 itemRead 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 resultsDelete 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 informationtenant- 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 itemitems.read- After reading itemsitems.update- After updating an itemitems.delete- Before deleting an item
Related Documentation
- Items API - CRUD operations that trigger hooks
- Schema API - Define fields needed for hooks
- Authentication API - Understand accountability context