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 nameaccountability- User and role informationaccountability.user- User object with id, email, firstName, etc.accountability.role- Role object with id, name, permissions
schema- Collection schema definitionsequelize- Sequelize instance for database operationstransaction- 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 objectreq.accountability.role- Current user's role
Parameters
req.params- URL parametersreq.query- Query string parametersreq.body- Request body datareq.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
- Always handle errors - Use try/catch and proper error responses
- Check permissions - Verify user authentication and authorization
- Use transactions - For operations that modify multiple records
- Return expected data - Follow the documented return value patterns
- Log important events - Use console.log for debugging and audit trails
- Validate input data - Check required fields and data types
- Use existing services - Leverage BAASIX services rather than direct database access