ObjectQL
Business Logic

Logic Hooks

Logic Hooks

Hooks (often called "Triggers" in SQL databases) allow you to intercept database operations to inject custom logic. They are transaction-aware and fully typed.

1. Registration Methods

You can define hooks in two ways: File-based (Static) or Programmatic (Dynamic).

Place a *.hook.ts file next to your object definition. The loader automatically discovers it.

File: src/objects/project.hook.ts

import { ObjectHookDefinition } from '@objectql/types';
 
const hooks: ObjectHookDefinition = {
    beforeCreate: async (ctx) => {
        // ...
    },
    afterUpdate: async (ctx) => {
        // ...
    }
};
 
export default hooks;

B. Programmatic (Dynamic)

Use the app.on() API, typically inside a Plugin.

app.on('before:create', 'project', async (ctx) => {
    // ...
});
 
// Wildcard listener
app.on('after:delete', '*', async (ctx) => {
    console.log(`Object ${ctx.objectName} deleted record ${ctx.id}`);
});

2. Event Lifecycle

Event NameDescriptionCommon Use Case
before:createBefore inserting a new record.Validation, Default Values, ID generation.
after:createAfter insertion is committed.Notifications, downstream sync.
before:updateBefore modifying an existing record.Permission checks, Immutable field protection.
after:updateAfter modification is committed.Audit logging, history tracking.
before:deleteBefore removing a record.Referential integrity checks.
after:deleteAfter removal is committed.Clean up related resources (e.g. S3 files).
before:findBefore executing a query.Row-Level Security (RLS), Force filters.
after:findAfter fetching results.Decryption, Sensitive data masking.

3. The Hook Context

The context object (ctx) changes based on the event type.

Common Properties (Available Everywhere)

PropertyTypeDescription
objectNamestringThe name of the object being operated on.
userObjectQLUserCurrent user session/context.
brokerIStation(If Microservices enabled) Station broker instance.

Mutation Context (Create/Update/Delete)

PropertyTypeAvailable InDescription
dataAnyCreate/UpdateThe data payload being written. Mutable.
idstringUpdate/DeleteThe ID of the record being acted upon.
previousDataAnyUpdate/DeleteThe existing record fetched from DB before operation.
resultAnyAfter *The final result returned from the driver.

Query Context (Find)

PropertyTypeDescription
querysteedos-filtersThe query AST (filters, fields, sort). Mutable.
resultAny[](After Find) The array of records found. Mutable.

4. Common Patterns & Examples

A. Validation & Default Values

Throwing an error inside a before hook aborts the transaction.

beforeCreate: async ({ data, user }) => {
    if (data.amount < 0) {
        throw new Error("Amount cannot be negative");
    }
    // Set default owner if not provided
    if (!data.owner) {
        data.owner = user.userId;
    }
}

B. Immutable Fields Protection

Prevent users from changing critical fields during update.

beforeUpdate: async ({ data, previousData }) => {
    if (data.code !== undefined && data.code !== previousData.code) {
        throw new Error("Cannot change project code once created.");
    }
}

C. Row-Level Security (RLS)

The most secure place to enforce permissions is before:find. This injects filters into every query (API, GraphQL, or internal).

beforeFind: async ({ query, user }) => {
    if (!user.is_admin) {
        // Enforce: owners can only see their own records
        // Merging into existing filters
        query.filters = [
            (query.filters || []), 
            ['owner', '=', user.userId]
        ];
    }
}

D. Side Effects (Notifications)

Use after hooks for logic that strictly relies on success.

afterCreate: async ({ data, objectName }) => {
    await NotificationService.send({
        to: data.owner,
        message: `New ${objectName} created.`
    });
}

E. Result Masking

Hide sensitive fields based on rules.

afterFind: async ({ result, user }) => {
    if (!user.has_permission('view_salary')) {
        result.forEach(record => {
            delete record.salary;
            delete record.bonus;
        });
    }
}

F. Auto-Numbering / ID Generation

Generate complex business keys.

beforeCreate: async ({ data }) => {
    if (!data.code) {
        data.code = await SequenceService.next('PROJECT_CODE');
    }
}

G. Conditional Deletion

Use previousData in delete hooks to prevent deleting records based on their state.

beforeDelete: async ({ previousData, user }) => {
    // Prevent deletion if project is active
    if (previousData.status === 'active') {
         throw new Error("Cannot delete an active project. Archive it first.");
    }
}

5. Transaction Safety

Hooks participate in the database transaction.

  • If a before hook throws -> The DB operation is never executed.
  • If the DB operation fails -> after hooks are never executed.
  • If an after hook throws -> The entire transaction rolls back (including the DB write).

Tip: If you want a "Fire and Forget" action that shouldn't rollback the transaction (e.g. sending an email), wrap your logic in a try/catch or execute it without await.

On this page