Lifecycle events for models and collections with both synchronous and asynchronous event handling, providing hooks for validation, data transformation, and custom business logic.
Core event handling methods available on both models and collections.
/**
* Register event listener(s)
* @param {string} events - Event name(s), space-separated for multiple
* @param {Function} callback - Event handler function
* @returns {this} Instance for chaining
*/
on(events: string, callback: Function): this;
/**
* Remove event listener(s)
* @param {string} [events] - Event name(s) to remove (all if not specified)
* @param {Function} [callback] - Specific callback to remove (all if not specified)
* @returns {this} Instance for chaining
*/
off(events?: string, callback?: Function): this;
/**
* Register one-time event listener
* @param {string} events - Event name(s), space-separated for multiple
* @param {Function} callback - One-time event handler
* @returns {this} Instance for chaining
*/
once(events: string, callback: Function): this;
/**
* Trigger synchronous event
* @param {string} event - Event name to trigger
* @param {...*} args - Arguments to pass to event handlers
* @returns {this} Instance for chaining
*/
trigger(event: string, ...args: any[]): this;
/**
* Trigger asynchronous event with promise handling
* @param {string} event - Event name to trigger
* @param {...*} args - Arguments to pass to event handlers
* @returns {Promise<any>} Promise resolving when all handlers complete
*/
triggerThen(event: string, ...args: any[]): Promise<any>;Usage Examples:
// Register event listeners
const user = new User();
user.on('saving', (model, attrs, options) => {
console.log('About to save user');
// Modify attributes before save
attrs.updated_at = new Date();
});
user.on('saved', (model, response, options) => {
console.log('User saved successfully');
});
// Multiple events with one handler
user.on('creating updating', (model) => {
model.set('modified_by', getCurrentUser().id);
});
// One-time listener
user.once('fetched', (model) => {
console.log('First fetch completed');
});
// Remove listeners
user.off('saving'); // Remove all saving listeners
user.off('saved', specificCallback); // Remove specific callback
user.off(); // Remove all listenersLifecycle events fired during model operations.
// Model lifecycle events (in order of execution)
/**
* Fired before fetching model from database
* @param {Model} model - Model being fetched
* @param {object} options - Fetch options
*/
'fetching': (model: Model, options: object) => void;
/**
* Fired after successfully fetching model
* @param {Model} model - Fetched model
* @param {object} response - Database response
* @param {object} options - Fetch options
*/
'fetched': (model: Model, response: object, options: object) => void;
/**
* Fired before saving model (create or update)
* @param {Model} model - Model being saved
* @param {object} attrs - Attributes being saved
* @param {object} options - Save options
*/
'saving': (model: Model, attrs: object, options: object) => void;
/**
* Fired before creating new model
* @param {Model} model - Model being created
* @param {object} attrs - Creation attributes
* @param {object} options - Create options
*/
'creating': (model: Model, attrs: object, options: object) => void;
/**
* Fired before updating existing model
* @param {Model} model - Model being updated
* @param {object} attrs - Update attributes
* @param {object} options - Update options
*/
'updating': (model: Model, attrs: object, options: object) => void;
/**
* Fired after successfully saving model
* @param {Model} model - Saved model
* @param {object} response - Database response
* @param {object} options - Save options
*/
'saved': (model: Model, response: object, options: object) => void;
/**
* Fired after successfully creating model
* @param {Model} model - Created model
* @param {object} response - Database response
* @param {object} options - Create options
*/
'created': (model: Model, response: object, options: object) => void;
/**
* Fired after successfully updating model
* @param {Model} model - Updated model
* @param {object} response - Database response
* @param {object} options - Update options
*/
'updated': (model: Model, response: object, options: object) => void;
/**
* Fired before destroying model
* @param {Model} model - Model being destroyed
* @param {object} options - Destroy options
*/
'destroying': (model: Model, options: object) => void;
/**
* Fired after successfully destroying model
* @param {Model} model - Destroyed model
* @param {object} response - Database response
* @param {object} options - Destroy options
*/
'destroyed': (model: Model, response: object, options: object) => void;Model Event Examples:
const User = bookshelf.model('User', {
tableName: 'users',
initialize() {
// Validation before save
this.on('saving', this.validateUser);
// Auto-set timestamps
this.on('creating', this.setCreatedAt);
this.on('updating', this.setUpdatedAt);
// Post-save hooks
this.on('created', this.welcomeNewUser);
this.on('destroyed', this.cleanupUserData);
},
validateUser(model, attrs, options) {
if (!attrs.email || !attrs.email.includes('@')) {
throw new Error('Valid email is required');
}
if (attrs.password && attrs.password.length < 8) {
throw new Error('Password must be at least 8 characters');
}
},
setCreatedAt(model, attrs) {
attrs.created_at = new Date();
},
setUpdatedAt(model, attrs) {
attrs.updated_at = new Date();
},
async welcomeNewUser(model) {
// Send welcome email
await sendWelcomeEmail(model.get('email'));
},
async cleanupUserData(model) {
// Clean up related data
await deleteUserFiles(model.id);
}
});Events fired during collection operations.
// Collection lifecycle events
/**
* Fired after successfully fetching collection
* @param {Collection} collection - Fetched collection
* @param {object} response - Database response
* @param {object} options - Fetch options
*/
'fetched': (collection: Collection, response: object, options: object) => void;
/**
* Fired when model is added to collection
* @param {Model} model - Added model
* @param {Collection} collection - Target collection
* @param {object} options - Add options
*/
'add': (model: Model, collection: Collection, options: object) => void;
/**
* Fired when model is removed from collection
* @param {Model} model - Removed model
* @param {Collection} collection - Source collection
* @param {object} options - Remove options
*/
'remove': (model: Model, collection: Collection, options: object) => void;
/**
* Fired when collection is reset with new models
* @param {Collection} collection - Reset collection
* @param {object} options - Reset options
*/
'reset': (collection: Collection, options: object) => void;
/**
* Fired when collection is sorted
* @param {Collection} collection - Sorted collection
* @param {object} options - Sort options
*/
'sort': (collection: Collection, options: object) => void;
/**
* Fired before creating model in collection
* @param {Model} model - Model being created
* @param {Collection} collection - Parent collection
* @param {object} options - Create options
*/
'creating': (model: Model, collection: Collection, options: object) => void;
/**
* Fired before attaching to pivot table (belongsToMany only)
* @param {Collection} collection - Collection being attached
* @param {Array} ids - IDs being attached
* @param {object} options - Attach options
*/
'attaching': (collection: Collection, ids: any[], options: object) => void;
/**
* Fired after attaching to pivot table (belongsToMany only)
* @param {Collection} collection - Collection after attach
* @param {Array} ids - IDs that were attached
* @param {object} options - Attach options
*/
'attached': (collection: Collection, ids: any[], options: object) => void;
/**
* Fired before detaching from pivot table (belongsToMany only)
* @param {Collection} collection - Collection being detached
* @param {Array} ids - IDs being detached
* @param {object} options - Detach options
*/
'detaching': (collection: Collection, ids: any[], options: object) => void;
/**
* Fired after detaching from pivot table (belongsToMany only)
* @param {Collection} collection - Collection after detach
* @param {Array} ids - IDs that were detached
* @param {object} options - Detach options
*/
'detached': (collection: Collection, ids: any[], options: object) => void;Collection Event Examples:
const Users = bookshelf.collection('Users', {
model: User,
initialize() {
// Collection-level event handlers
this.on('add', this.onUserAdded);
this.on('remove', this.onUserRemoved);
this.on('reset', this.onCollectionReset);
this.on('fetched', this.onFetched);
},
onUserAdded(user, collection, options) {
console.log(`User ${user.get('name')} added to collection`);
this.trigger('collection:changed');
},
onUserRemoved(user, collection, options) {
console.log(`User ${user.get('name')} removed from collection`);
this.trigger('collection:changed');
},
onCollectionReset(collection, options) {
console.log(`Collection reset with ${collection.length} users`);
},
onFetched(collection, response, options) {
console.log(`Fetched ${collection.length} users from database`);
}
});
// Many-to-many pivot events
const user = new User({id: 1});
user.roles().on('attaching', (collection, ids) => {
console.log('Attaching roles:', ids);
});
user.roles().on('attached', (collection, ids) => {
console.log('Successfully attached roles:', ids);
});Handle events that return promises with triggerThen.
// Asynchronous event patterns
// Event handler returning promise
model.on('saving', async (model, attrs, options) => {
// Async validation
const isValid = await validateWithExternalService(attrs);
if (!isValid) {
throw new Error('External validation failed');
}
});
// Trigger async event and wait for all handlers
await model.triggerThen('custom:event', data);Async Event Examples:
const User = bookshelf.model('User', {
tableName: 'users',
initialize() {
// Async event handlers
this.on('saving', this.hashPassword);
this.on('created', this.createProfile);
this.on('destroyed', this.cleanupAsync);
},
async hashPassword(model, attrs) {
if (attrs.password) {
attrs.password = await bcrypt.hash(attrs.password, 10);
}
},
async createProfile(model) {
// Create related profile asynchronously
await new Profile({
user_id: model.id,
display_name: model.get('name')
}).save();
},
async cleanupAsync(model) {
// Multiple async cleanup operations
await Promise.all([
deleteS3Files(model.id),
removeFromCache(model.id),
notifyServices(model.id)
]);
}
});
// Using triggerThen for custom events
const user = new User();
user.on('custom:validation', async (data) => {
const result = await externalValidation(data);
return result;
});
// Wait for all async handlers to complete
const results = await user.triggerThen('custom:validation', userData);Events propagate through the model hierarchy and can be stopped.
// Stop event propagation
model.on('saving', (model, attrs, options) => {
if (someCondition) {
// Prevent save operation
return false;
}
});
// Events bubble up through relations
const post = new Post();
post.comments().on('add', (comment) => {
// Fired when comment added to post's comments collection
post.trigger('comment:added', comment);
});// Global event handling
bookshelf.Model.prototype.initialize = function() {
// Applied to all models
this.on('saving', function(model) {
console.log('Model being saved:', model.tableName);
});
};
// Plugin-style global events
bookshelf.plugin(function(bookshelf) {
bookshelf.Model.extend({
initialize() {
this.on('fetching', this.logFetch);
},
logFetch(model, options) {
console.log(`Fetching ${model.tableName}`);
}
});
});const User = bookshelf.model('User', {
initialize() {
this.on('saving', this.validate);
},
validate(model, attrs, options) {
// Validation errors stop the save operation
if (!attrs.email) {
throw new bookshelf.ValidationError('Email is required');
}
// Async validation with promises
return validateEmailUnique(attrs.email)
.then(isUnique => {
if (!isUnique) {
throw new Error('Email already exists');
}
});
}
});