Accessor Hooks
Accessor Hooks are executed at the DataStore level whenever an entity is read from or written to the database. Unlike Resolver Hooks which operate at the GraphQL API level, Accessor Hooks provide lower-level control over database operations.
Overview
Accessor Hooks are useful when you need to:
- Modify data before it's written to the database
- Transform data after it's read from the database
- Add validation at the database layer
- Track changes for auditing purposes
- Implement cross-cutting concerns like encryption
Available Hooks
The EntityAccessor provides the following hooks:
| Hook | Execution Point | Purpose |
|---|---|---|
beforeCreate | Before creating a new item in DB | Validate/modify data before insert |
afterCreate | After creating a new item in DB | Post-creation tasks, logging |
beforeUpdate | Before updating an item in DB | Validate/modify data before update |
afterUpdate | After updating an item in DB | Post-update tasks, change tracking |
beforeDelete | Before deleting an item from DB | Pre-delete validation, soft delete |
afterDelete | After deleting an item from DB | Cleanup, cascade operations |
Hook Registration
Add hooks using the entity's onInit method:
export const domainConfiguration: DomainConfiguration = {
entity: {
Car: {
onInit: entity => {
entity.accessor.hooks.beforeCreate.push((params: AccessorHookParams) => {
// Modify item before it's saved to database
if (!params.item.color) {
params.item.color = 'unknown';
}
});
entity.accessor.hooks.afterUpdate.push((params: AccessorHookParams) => {
// Log changes after update
console.log(`Car ${params.item.id} was updated`);
});
}
}
}
};AccessorHookParams
Each hook receives an AccessorHookParams object:
interface AccessorHookParams {
item: EntityItem; // The entity item being processed
origin?: EntityItem; // Original item (for updates)
principal?: Principal; // Current user/principal
runtime: Runtime; // ActiveQL runtime instance
}Hook Examples
Before Create Hook
Validate or set default values before creating:
entity.accessor.hooks.beforeCreate.push((params) => {
const { item, principal, runtime } = params;
// Automatically set creator
item.createdBy = principal?.id;
// Generate unique identifier
if (!item.license) {
item.license = generateUniqueLicense();
}
// Validate business rules
if (item.mileage < 0) {
throw new Error('Mileage cannot be negative');
}
});After Create Hook
Perform post-creation tasks:
entity.accessor.hooks.afterCreate.push(async (params) => {
const { item, runtime } = params;
// Create related records
await runtime.entity('MaintenanceSchedule').create({
car: item.id,
nextService: addMonths(new Date(), 6)
});
// Send notification
await sendNotification({
type: 'car_created',
carId: item.id
});
});Before Update Hook
Compare old and new values:
entity.accessor.hooks.beforeUpdate.push((params) => {
const { item, origin } = params;
// Track significant changes
if (origin.mileage && item.mileage < origin.mileage) {
throw new Error('Mileage cannot decrease');
}
// Automatically update modifier
item.updatedBy = params.principal?.id;
// Log change
if (item.status !== origin.status) {
console.log(`Status changed from ${origin.status} to ${item.status}`);
}
});After Update Hook
Post-update processing:
entity.accessor.hooks.afterUpdate.push(async (params) => {
const { item, origin, runtime } = params;
// If status changed, trigger workflow
if (item.status !== origin.status) {
await runtime.entity('StatusHistory').create({
car: item.id,
oldStatus: origin.status,
newStatus: item.status,
changedAt: new Date()
});
}
});Before Delete Hook
Implement soft delete or validation:
entity.accessor.hooks.beforeDelete.push((params) => {
const { item, principal } = params;
// Prevent deletion by non-admins
if (!principal?.roles?.includes('admin')) {
throw new Error('Only admins can delete cars');
}
// Soft delete instead
if (item.hasActiveRentals) {
throw new Error('Cannot delete car with active rentals');
}
});After Delete Hook
Cleanup related data:
entity.accessor.hooks.afterDelete.push(async (params) => {
const { item, runtime } = params;
// Delete related maintenance records
const maintenanceRecords = await runtime.entity('Maintenance')
.findByAttribute(item.id, 'car');
for (const record of maintenanceRecords) {
await runtime.entity('Maintenance').delete(record.id);
}
// Log deletion
console.log(`Car ${item.id} and related records deleted`);
});Advanced Patterns
Auditing / Change Tracking
entity.accessor.hooks.beforeUpdate.push((params) => {
const { item, origin, principal } = params;
// Track all changes
const changes = {};
Object.keys(item).forEach(key => {
if (item[key] !== origin[key]) {
changes[key] = {
old: origin[key],
new: item[key]
};
}
});
// Store audit trail
item._auditTrail = {
changedBy: principal?.id,
changedAt: new Date(),
changes
};
});Data Encryption
entity.accessor.hooks.beforeCreate.push((params) => {
// Encrypt sensitive data before storing
if (params.item.vin) {
params.item.vin = encrypt(params.item.vin);
}
});
entity.accessor.hooks.afterCreate.push((params) => {
// Decrypt after reading
if (params.item.vin) {
params.item.vin = decrypt(params.item.vin);
}
});Cross-Entity Validation
entity.accessor.hooks.beforeCreate.push(async (params) => {
const { item, runtime } = params;
// Check if driver's license is valid
const driver = await runtime.entity('Driver').findById(item.assignedDriver);
if (!driver?.licenseValid) {
throw new Error('Cannot assign car to driver with invalid license');
}
});Accessor Hooks vs Resolver Hooks
| Aspect | Accessor Hooks | Resolver Hooks |
|---|---|---|
| Level | DataStore/Database | GraphQL API |
| When executed | On every DB operation | On GraphQL query/mutation |
| Access to | Entity item, origin, principal | Full GraphQL context, args |
| Use case | Data transformation, DB-level validation | API-level business logic, authorization |
| Performance | Executes for internal operations too | Only for API calls |
| Scope | Low-level, data-focused | High-level, API-focused |
Rule of thumb:
- Use Accessor Hooks for data integrity, transformation, and DB-level concerns
- Use Resolver Hooks for API-specific business logic, complex authorization, and GraphQL-level concerns
Multiple Hooks
You can register multiple hooks - they execute in the order they were added:
entity.accessor.hooks.beforeCreate.push((params) => {
console.log('Hook 1');
});
entity.accessor.hooks.beforeCreate.push((params) => {
console.log('Hook 2');
});
entity.accessor.hooks.beforeCreate.push((params) => {
console.log('Hook 3');
});
// Executes: Hook 1 → Hook 2 → Hook 3Error Handling
Throwing an error in a hook will abort the operation:
entity.accessor.hooks.beforeCreate.push((params) => {
if (!params.item.requiredField) {
throw new Error('Required field is missing');
// Operation aborted, nothing is saved
}
});Async Hooks
Hooks can be async:
entity.accessor.hooks.afterCreate.push(async (params) => {
await sendEmailNotification(params.item);
await updateExternalSystem(params.item);
});See Also
- Resolver Hooks - API-level hooks
- Entity Manager - CRUD operations
- Entity Configuration - Entity setup
- Principal - Authentication and authorization