Skip to content

Commit 37da16c

Browse files
committed
Revamp hook spec and types for ObjectQL
Expanded and clarified hook documentation to cover design philosophy, context API, and implementation patterns. Refactored hook type definitions for improved type safety, context awareness, and change tracking, introducing new context interfaces and a unified ObjectHookDefinition structure.
1 parent 17ae91b commit 37da16c

2 files changed

Lines changed: 210 additions & 36 deletions

File tree

docs/spec/hook.md

Lines changed: 91 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,103 @@
11
# Hooks (Triggers)
22

3-
Hooks allow you to execute server-side logic before or after database operations. They are defined in a separate `*.hook.ts` file or registered dynamically.
3+
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.
44

5-
## 1. Supported Hooks
5+
## 1. Overview
66

7-
| Hook | Description | Context Properties |
8-
| :--- | :--- | :--- |
9-
| `beforeFind` | Before a query is executed. | `query` |
10-
| `afterFind` | After a query is executed (results available). | `query`, `result` |
11-
| `beforeCreate` | Before a record is inserted. | `doc` |
12-
| `afterCreate` | After a record is inserted. | `doc`, `result`, `id` |
13-
| `beforeUpdate` | Before a record is updated. | `id`, `doc`, `query` |
14-
| `afterUpdate` | After a record is updated. | `id`, `doc`, `result` |
15-
| `beforeDelete` | Before a record is deleted. | `id`, `query` |
16-
| `afterDelete` | After a record is deleted. | `id`, `result` |
7+
Hook files should be named `[object_name].hook.ts` and placed alongside your `*.object.yml` files.
178

18-
> **Note on Aggregation:**
19-
> If `query.aggregate` or `query.groupBy` is present (Aggregation Query), the `result` in `afterFind` will be an array of raw aggregation objects (e.g. `[{ total: 100, category: 'A' }]`) instead of standard object instances.
9+
### The "Optimal" Design Philosophy
2010

21-
## 2. Hook Implementation
11+
Unlike traditional ORMs that provide generic contexts, ObjectQL hooks are **Typed**, **Context-Aware**, and **Smart**.
12+
13+
* **Type Safety**: Contexts are generic (e.g., `UpdateHookContext<Project>`), giving you autocomplete for fields.
14+
* **Separation of Concerns**: `before` hooks focus on validation/mutation; `after` hooks focus on side-effects.
15+
* **Change Tracking**: Built-in helpers like `isModified()` simplify "diff" logic.
16+
17+
## 2. Supported Hooks
18+
19+
| Hook | Operation | Context Properties | Purpose |
20+
| :--- | :--- | :--- | :--- |
21+
| `beforeFind` | Find/Count | `query` | Modify query filters, enforce security. |
22+
| `afterFind` | Find/Count | `query`, `result` | Transform results, logging. |
23+
| `beforeCreate` | Create | `data` | Validate inputs, set defaults, calculate fields. |
24+
| `afterCreate` | Create | `data`, `result` | Send welcome emails, create related records. |
25+
| `beforeUpdate` | Update | `id`, `data`, `previousData` | Validate state transitions (e.g., draft -> published). |
26+
| `afterUpdate` | Update | `id`, `data`, `previousData` | Notifications based on changes. |
27+
| `beforeDelete` | Delete | `id` | Check dependency constraints. |
28+
| `afterDelete` | Delete | `id`, `result` | Cleanup external resources (S3 files, etc). |
29+
30+
## 3. Implementation
31+
32+
The recommended way to define hooks is using the `ObjectHookDefinition` interface.
2233

2334
```typescript
24-
import { ObjectQL } from '@objectql/core';
35+
// src/objects/project.hook.ts
36+
import { ObjectHookDefinition } from '@objectql/types';
37+
import { Project } from './types'; // Your generated type
38+
39+
const hooks: ObjectHookDefinition<Project> = {
40+
41+
// 1. Validation & Defaulting
42+
beforeCreate: async ({ data, user, api }) => {
43+
if (!data.name) {
44+
throw new Error("Project name is required");
45+
}
46+
47+
// Auto-assign owner
48+
data.owner_id = user?.id;
49+
50+
// Check uniqueness via API
51+
const existing = await api.count('project', [['name', '=', data.name]]);
52+
if (existing > 0) throw new Error("Name taken");
53+
},
2554

26-
// Inside your server-side loader
27-
const objectql = new ObjectQL();
55+
// 2. State Transition Logic
56+
beforeUpdate: async ({ data, previousData, isModified }) => {
57+
// 'previousData' is automatically fetched by the engine
58+
59+
if (isModified('status')) {
60+
if (previousData.status === 'Completed' && data.status !== 'Completed') {
61+
throw new Error("Cannot reopen a completed project");
62+
}
63+
}
64+
},
2865

29-
objectql.registerHook('projects', 'beforeCreate', async (ctx) => {
30-
if (ctx.doc.budget < 0) {
31-
throw new Error("Budget cannot be negative");
66+
// 3. Side Effects (Notifications)
67+
afterUpdate: async ({ isModified, data, api }) => {
68+
if (isModified('status') && data.status === 'Completed') {
69+
await api.create('notification', {
70+
message: `Project ${data.name} finished!`,
71+
user_id: data.owner_id
72+
});
73+
}
3274
}
33-
});
75+
};
76+
77+
export default hooks;
78+
```
79+
80+
## 4. Hook Context API
81+
82+
The context object passed to your function is tailored to the operation.
83+
84+
### 4.1 Base Properties (Available Everywhere)
85+
* `objectName`: string
86+
* `api`: The internal ObjectQL driver instance (for running queries).
87+
* `user`: The current user session.
88+
* `state`: A shared object to pass data from `before` to `after` hooks.
89+
90+
### 4.2 Update Context (`beforeUpdate` / `afterUpdate`)
91+
* `data`: The partial object containing changes.
92+
* `previousData`: The full record **before** the update.
93+
* `isModified(field)`: Returns `true` if the field is present in `data` AND different from `previousData`.
94+
95+
### 4.3 Query Context (`beforeFind`)
96+
* `query`: The AST of the query. You can inject extra filters here.
97+
98+
```typescript
99+
beforeFind: async ({ query, user }) => {
100+
// Force multi-tenancy filter
101+
query.filters.push(['organization_id', '=', user.org_id]);
102+
}
34103
```

packages/types/src/hook.ts

Lines changed: 119 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,125 @@
1-
import { ObjectQLContext } from "./types";
21
import { UnifiedQuery } from "./query";
32

4-
export type HookName =
5-
| 'beforeFind' | 'afterFind'
6-
| 'beforeCreate' | 'afterCreate'
7-
| 'beforeUpdate' | 'afterUpdate'
8-
| 'beforeDelete' | 'afterDelete'
9-
| 'beforeCount' | 'afterCount';
3+
/**
4+
* Standard CRUD operations supported by hooks.
5+
*/
6+
export type HookOperation = 'find' | 'count' | 'create' | 'update' | 'delete';
107

11-
export interface HookContext extends ObjectQLContext {
8+
/**
9+
* Execution timing relative to the database operation.
10+
*/
11+
export type HookTiming = 'before' | 'after';
12+
13+
/**
14+
* Minimal API surface exposed to hooks for performing side-effects or checks.
15+
*/
16+
export interface HookAPI {
17+
// We use 'any' here to avoid circular dependencies with core/ObjectQL
18+
// In practice, this is the ObjectQL instance.
19+
find(objectName: string, query?: any): Promise<any[]>;
20+
findOne(objectName: string, id: string | number): Promise<any>;
21+
count(objectName: string, query?: any): Promise<number>;
22+
create(objectName: string, data: any): Promise<any>;
23+
update(objectName: string, id: string | number, data: any): Promise<any>;
24+
delete(objectName: string, id: string | number): Promise<any>;
25+
}
26+
27+
/**
28+
* Base context available in all hooks.
29+
*/
30+
export interface BaseHookContext<T = any> {
31+
/** The name of the object (entity) being acted upon. */
1232
objectName: string;
13-
query?: UnifiedQuery; // For find/count
14-
doc?: any; // For create/update
15-
id?: string | number; // For update/delete/findOne
16-
result?: any; // For after* hooks
17-
meta?: any; // To pass data between hooks or from main context
33+
34+
/** The triggering operation. */
35+
operation: HookOperation;
36+
37+
/** Access to the database/engine to perform extra queries. */
38+
api: HookAPI;
39+
40+
/** User/Session context (Authentication info). */
41+
user?: {
42+
id: string | number;
43+
[key: string]: any;
44+
};
45+
46+
/**
47+
* Shared state for passing data between matching 'before' and 'after' hooks.
48+
* e.g. Calculate a diff in 'beforeUpdate' and read it in 'afterUpdate'.
49+
*/
50+
state: Record<string, any>;
51+
}
52+
53+
/**
54+
* Context for Retrieval operations (Find, Count).
55+
*/
56+
export interface RetrievalHookContext<T = any> extends BaseHookContext<T> {
57+
operation: 'find' | 'count';
58+
59+
/** The query criteria being executed. Modifiable in 'before' hooks. */
60+
query: UnifiedQuery;
61+
62+
/** The result of the query. Only available in 'after' hooks. */
63+
result?: T[] | number;
1864
}
1965

20-
export type HookHandler = (ctx: HookContext) => Promise<void> | void;
66+
/**
67+
* Context for Modification operations (Create, Update, Delete).
68+
*/
69+
export interface MutationHookContext<T = any> extends BaseHookContext<T> {
70+
operation: 'create' | 'update' | 'delete';
71+
72+
/** The record ID. Undefined for 'create'. */
73+
id?: string | number;
74+
75+
/**
76+
* The incoming data changes.
77+
* - For 'create': The full object to insert.
78+
* - For 'update': The partial fields to update.
79+
* - For 'delete': Undefined.
80+
*/
81+
data?: Partial<T>;
82+
83+
/**
84+
* The final result record from the database.
85+
* Only available in 'after' hooks.
86+
*/
87+
result?: T;
88+
}
89+
90+
/**
91+
* Specialized context for Updates, including change tracking.
92+
*/
93+
export interface UpdateHookContext<T = any> extends MutationHookContext<T> {
94+
operation: 'update';
95+
96+
/**
97+
* The record state BEFORE the update.
98+
* Useful for comparison logic (e.g. status changed from A to B).
99+
* Note: This may require a pre-fetch lookup depending on engine configuration.
100+
*/
101+
previousData?: T;
102+
103+
/**
104+
* Helper to check if a specific field is being modified.
105+
* Checks if the field exists in 'data' AND is different from 'previousData'.
106+
*/
107+
isModified(field: keyof T): boolean;
108+
}
109+
110+
/**
111+
* Definition interface for a set of hooks for a specific object.
112+
*/
113+
export interface ObjectHookDefinition<T = any> {
114+
beforeFind?: (ctx: RetrievalHookContext<T>) => Promise<void> | void;
115+
afterFind?: (ctx: RetrievalHookContext<T>) => Promise<void> | void;
116+
117+
beforeDelete?: (ctx: MutationHookContext<T>) => Promise<void> | void;
118+
afterDelete?: (ctx: MutationHookContext<T>) => Promise<void> | void;
119+
120+
beforeCreate?: (ctx: MutationHookContext<T>) => Promise<void> | void;
121+
afterCreate?: (ctx: MutationHookContext<T>) => Promise<void> | void;
122+
123+
beforeUpdate?: (ctx: UpdateHookContext<T>) => Promise<void> | void;
124+
afterUpdate?: (ctx: UpdateHookContext<T>) => Promise<void> | void;
125+
}

0 commit comments

Comments
 (0)