Triggers
BucketDB supports registering runtime triggers that fire synchronously during batch operations (insert, update, delete). Triggers allow you to enforce business logic, maintain materialized views, or cascade changes automatically within the same atomic transaction.
Registering Triggers
Triggers are registered directly on the db instance and are evaluated during the operation (e.g. batch.insert()), _before_ the row is encoded and flushed.
db.registerTrigger('orders', 'insert', async (row, batch, dbApi, operation) => {
// Validate data
if (row.amount < 0) throw new Error('Invalid order amount');
// Maintain a materialized view synchronously
const results = await batch.query('daily_sales').where('date', '=', row.date).execute();
if (results[0]) {
await batch.update('daily_sales', { date: row.date, total: results[0].total + row.amount });
} else {
await batch.insert('daily_sales', { date: row.date, total: row.amount });
}
});
Trigger Handler Signature
async (row, batch, dbApi, operation) => void
row(Object): The data object being mutated. Forinsertandupdate, this contains the fields passed to the method. Fordelete, it contains the primary key (e.g.,{ pk: '123' }).batch(Batch): The current transaction context. Queries and mutations made on this batch will see the unflushed state of the transaction.dbApi(Database): The main database instance API, used to access utility functions likegetBlob.operation(String): The operation type ('insert','update','delete').
Working with Managed Blobs (typeId: 11)
When your schema includes a Managed Blob field (typeId: 11), handling it inside a trigger requires understanding how BucketDB processes blobs.
Blobs are stored externally in S3, while the database row simply stores a UUID reference to the blob. Because triggers fire _before_ encoding, the shape of the blob data in row depends on the operation and how the user passed it.
Scenario A: Reading a Newly Uploaded Blob (Insert / Update)
If the user is inserting or updating the blob field, they pass a raw Buffer or a db.Blob(buffer) wrapper to the batch operation. Because the trigger fires before the encoder replaces the buffer with a UUID, the blob is available directly in memory on the row object.
db.registerTrigger('users', 'insert', async (row, batch, dbApi) => {
const avatarBlob = row.avatar; // The raw Buffer or db.Blob() object
if (avatarBlob) {
// Extract the raw buffer safely
const buffer = Buffer.isBuffer(avatarBlob) ? avatarBlob : avatarBlob.value;
if (buffer.length > 5 * 1024 * 1024) {
throw new Error('Avatar cannot exceed 5MB');
}
}
});
Note: Do NOT attempt to use dbApi.getBlob() on a newly inserted blob, as the UUID has not yet been generated and the blob has not been uploaded to S3.
Scenario B: Reading an Existing Blob from Storage (Update / Delete)
If a trigger fires on an update or delete operation, the user's row object might only contain the fields they are changing. If you need to inspect an existing blob that was saved previously, you must query the database to get its UUID reference, then use dbApi.getBlob(reference) to fetch it from S3.
db.registerTrigger('users', 'delete', async (row, batch, dbApi) => {
// `row` only contains the primary key for a delete: { pk: 'user_123' }
const existingUsers = await batch.query('users').where('id', '=', row.pk).execute();
const existingUser = existingUsers[0];
if (existingUser && existingUser.avatar) {
// The query returns the UUID string for the blob
const blobRef = dbApi.getBlob(existingUser.avatar);
// Download the blob into memory (or string, depending on your needs)
const buffer = await blobRef.getBuffer();
// ... do something with the existing blob ...
}
});
Summary of Blob Handling in Triggers
- New blobs (Insert/Update): Access the buffer directly via
row.fieldName. It is fully in memory. - Existing blobs (Update/Delete): Query the row to get the UUID, then use
dbApi.getBlob(uuid).getBuffer()to download it. - Recursion Limit: Triggers have a maximum call-stack depth of 10 to prevent infinite recursion loops. Exceeding this throws a
TriggerRecursionError.