Implementing ActiveQL
Accessor Hooks

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:

HookExecution PointPurpose
beforeCreateBefore creating a new item in DBValidate/modify data before insert
afterCreateAfter creating a new item in DBPost-creation tasks, logging
beforeUpdateBefore updating an item in DBValidate/modify data before update
afterUpdateAfter updating an item in DBPost-update tasks, change tracking
beforeDeleteBefore deleting an item from DBPre-delete validation, soft delete
afterDeleteAfter deleting an item from DBCleanup, 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

AspectAccessor HooksResolver Hooks
LevelDataStore/DatabaseGraphQL API
When executedOn every DB operationOn GraphQL query/mutation
Access toEntity item, origin, principalFull GraphQL context, args
Use caseData transformation, DB-level validationAPI-level business logic, authorization
PerformanceExecutes for internal operations tooOnly for API calls
ScopeLow-level, data-focusedHigh-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 3

Error 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