Hooks allow you to execute server-side logic before or after database operations. They are the primary mechanism for implementing business logic, validation, and side effects in ObjectQL.
File Naming Convention: <object_name>.hook.ts
Hook implementation files should be named to match the object they apply to, and placed alongside your object definition files.
Examples:
project.hook.ts→ Hooks forprojectobjectcustomer_order.hook.ts→ Hooks forcustomer_orderobject
Unlike traditional ORMs that provide generic contexts, ObjectQL hooks are Typed, Context-Aware, and Smart.
- Type Safety: Contexts are generic (e.g.,
UpdateHookContext<Project>), giving you autocomplete for fields. - Separation of Concerns:
beforehooks focus on validation/mutation;afterhooks focus on side-effects. - Change Tracking: Built-in helpers like
isModified()simplify "diff" logic.
| Hook | Operation | Context Properties | Purpose |
|---|---|---|---|
beforeFind |
Find/Count | query |
Modify query filters, enforce security. |
afterFind |
Find/Count | query, result |
Transform results, logging. |
beforeCreate |
Create | data |
Validate inputs, set defaults, calculate fields. |
afterCreate |
Create | data, result |
Send welcome emails, create related records. |
beforeUpdate |
Update | id, data, previousData |
Validate state transitions (e.g., draft -> published). |
afterUpdate |
Update | id, data, previousData |
Notifications based on changes. |
beforeDelete |
Delete | id |
Check dependency constraints. |
afterDelete |
Delete | id, result |
Cleanup external resources (S3 files, etc). |
The recommended way to define hooks is using the ObjectHookDefinition interface.
// File: project.hook.ts
// Hooks for the "project" object (name matches object definition file)
import { ObjectHookDefinition } from '@objectql/types';
import { Project } from './types'; // Your generated type
const hooks: ObjectHookDefinition<Project> = {
// 1. Validation & Defaulting
beforeCreate: async ({ data, user, api }) => {
if (!data.name) {
throw new Error("Project name is required");
}
// Auto-assign owner
data.owner_id = user?.id;
// Check uniqueness via API
const existing = await api.count('project', [['name', '=', data.name]]);
if (existing > 0) throw new Error("Name taken");
},
// 2. State Transition Logic
beforeUpdate: async ({ data, previousData, isModified }) => {
// 'previousData' is automatically fetched by the engine
if (isModified('status')) {
if (previousData.status === 'Completed' && data.status !== 'Completed') {
throw new Error("Cannot reopen a completed project");
}
}
},
// 3. Side Effects (Notifications)
afterUpdate: async ({ isModified, data, api }) => {
if (isModified('status') && data.status === 'Completed') {
await api.create('notification', {
message: `Project ${data.name} finished!`,
user_id: data.owner_id
});
}
}
};
export default hooks;The context object passed to your function is tailored to the operation.
objectName: stringapi: The internal ObjectQL driver instance (for running queries).user: The current user session.state: A shared object to pass data frombeforetoafterhooks.
data: The partial object containing changes.previousData: The full record before the update.isModified(field): Returnstrueif the field is present indataAND different frompreviousData.
query: The AST of the query. You can inject extra filters here.
beforeFind: async ({ query, user }) => {
// Force multi-tenancy filter
query.filters.push(['organization_id', '=', user.org_id]);
}Hooks follow the same convention-based loading strategy as Actions.
- File Name:
[object_name].hook.ts(e.g.,user.hook.ts) - Exports: Must export a default object complying with
ObjectHookDefinition, or named exports matching hook methods.
// src/objects/user.hook.ts
import { ObjectHookDefinition } from '@objectql/types';
const hooks: ObjectHookDefinition = {
beforeCreate: async (ctx) => { ... }
};
export default hooks;