Skip to content

Commit 4559cff

Browse files
committed
feat: 添加钩子生命周期事件和钩子定义模式,支持数据访问生命周期中的自定义逻辑
1 parent 573ddd8 commit 4559cff

File tree

6 files changed

+214
-739
lines changed

6 files changed

+214
-739
lines changed

packages/objectql/src/index.ts

Lines changed: 113 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,20 @@ import { SchemaRegistry } from './registry';
66
// Export Registry for consumers
77
export { SchemaRegistry } from './registry';
88

9+
/**
10+
* Hook Context
11+
*/
12+
export interface HookContext {
13+
object: string;
14+
driver: DriverInterface;
15+
method: 'find' | 'insert' | 'update' | 'delete' | 'count';
16+
args: any; // The arguments passed to the method (can be modified)
17+
result?: any; // The result of the operation (for after hooks)
18+
error?: any; // The error if one occurred (for error hooks)
19+
}
20+
21+
export type HookHandler = (context: HookContext) => Promise<void> | void;
22+
923
/**
1024
* Host Context provided to plugins
1125
*/
@@ -23,6 +37,14 @@ export class ObjectQL {
2337
private drivers = new Map<string, DriverInterface>();
2438
private defaultDriver: string | null = null;
2539

40+
// Hooks Registry
41+
private hooks: Record<string, HookHandler[]> = {
42+
'beforeFind': [], 'afterFind': [],
43+
'beforeInsert': [], 'afterInsert': [],
44+
'beforeUpdate': [], 'afterUpdate': [],
45+
'beforeDelete': [], 'afterDelete': [],
46+
};
47+
2648
// Host provided context additions (e.g. Server router)
2749
private hostContext: Record<string, any> = {};
2850

@@ -94,6 +116,27 @@ export class ObjectQL {
94116
}
95117
}
96118

119+
/**
120+
* Register a hook
121+
* @param event The event name (e.g. 'beforeFind', 'afterInsert')
122+
* @param handler The handler function
123+
*/
124+
registerHook(event: string, handler: HookHandler) {
125+
if (!this.hooks[event]) {
126+
this.hooks[event] = [];
127+
}
128+
this.hooks[event].push(handler);
129+
console.log(`[ObjectQL] Registered hook for ${event}`);
130+
}
131+
132+
private async triggerHooks(event: string, context: HookContext) {
133+
const handlers = this.hooks[event] || [];
134+
for (const handler of handlers) {
135+
// In a real system, we might want to catch errors here or allow them to bubble up
136+
await handler(context);
137+
}
138+
}
139+
97140
/**
98141
* Register a new storage driver
99142
*/
@@ -189,27 +232,35 @@ export class ObjectQL {
189232
// Normalize QueryAST
190233
let ast: QueryAST;
191234
if (query.where || query.fields || query.orderBy || query.limit) {
192-
// It's likely a QueryAST or partial QueryAST
193-
// Ensure 'object' is set correctly
194-
ast = {
195-
object, // Force object name to match the call
196-
...query
197-
} as QueryAST;
235+
ast = { object, ...query } as QueryAST;
198236
} else {
199-
// It's a direct filter object (Simplified syntax)
200-
// e.g. find('account', { name: 'Acme' })
201-
ast = {
202-
object,
203-
where: query
204-
} as QueryAST;
237+
ast = { object, where: query } as QueryAST;
205238
}
206239

207-
// Default limit protection
208-
if (ast.limit === undefined) {
209-
ast.limit = 100;
210-
}
240+
if (ast.limit === undefined) ast.limit = 100;
211241

212-
return driver.find(object, ast, options);
242+
// Trigger Before Hook
243+
const hookContext: HookContext = {
244+
object,
245+
driver,
246+
method: 'find',
247+
args: { ast, options } // Hooks can modify AST here
248+
};
249+
await this.triggerHooks('beforeFind', hookContext);
250+
251+
try {
252+
const result = await driver.find(object, hookContext.args.ast, hookContext.args.options);
253+
254+
// Trigger After Hook
255+
hookContext.result = result;
256+
await this.triggerHooks('afterFind', hookContext);
257+
258+
return hookContext.result;
259+
} catch (e) {
260+
hookContext.error = e;
261+
// await this.triggerHooks('error', hookContext);
262+
throw e;
263+
}
213264
}
214265

215266
async findOne(object: string, idOrQuery: string | any, options?: DriverOptions) {
@@ -247,21 +298,60 @@ export class ObjectQL {
247298
// validate(schema, data);
248299
}
249300

250-
// 2. Run "Before Insert" Triggers
301+
// 2. Trigger Before Hook
302+
const hookContext: HookContext = {
303+
object,
304+
driver,
305+
method: 'insert',
306+
args: { data, options }
307+
};
308+
await this.triggerHooks('beforeInsert', hookContext);
251309

252-
const result = await driver.create(object, data, options);
310+
// 3. Execute Driver
311+
const result = await driver.create(object, hookContext.args.data, hookContext.args.options);
253312

254-
// 3. Run "After Insert" Triggers
255-
return result;
313+
// 4. Trigger After Hook
314+
hookContext.result = result;
315+
await this.triggerHooks('afterInsert', hookContext);
316+
317+
return hookContext.result;
256318
}
257319

258320
async update(object: string, id: string | number, data: Record<string, any>, options?: DriverOptions) {
259321
const driver = this.getDriver(object);
260-
return driver.update(object, id, data, options);
322+
323+
const hookContext: HookContext = {
324+
object,
325+
driver,
326+
method: 'update',
327+
args: { id, data, options }
328+
};
329+
await this.triggerHooks('beforeUpdate', hookContext);
330+
331+
const result = await driver.update(object, hookContext.args.id, hookContext.args.data, hookContext.args.options);
332+
333+
hookContext.result = result;
334+
await this.triggerHooks('afterUpdate', hookContext);
335+
336+
return hookContext.result;
261337
}
262338

263339
async delete(object: string, id: string | number, options?: DriverOptions) {
264340
const driver = this.getDriver(object);
265-
return driver.delete(object, id, options);
341+
342+
const hookContext: HookContext = {
343+
object,
344+
driver,
345+
method: 'delete',
346+
args: { id, options }
347+
};
348+
await this.triggerHooks('beforeDelete', hookContext);
349+
350+
const result = await driver.delete(object, hookContext.args.id, hookContext.args.options);
351+
352+
hookContext.result = result;
353+
await this.triggerHooks('afterDelete', hookContext);
354+
355+
return hookContext.result;
266356
}
267357
}

packages/spec/src/data/hook.zod.ts

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { z } from 'zod';
2+
3+
/**
4+
* Hook Lifecycle Events
5+
* Defines the interception points in the ObjectQL execution pipeline.
6+
*/
7+
export const HookEvent = z.enum([
8+
// Read Operations
9+
'beforeFind', 'afterFind',
10+
'beforeFindOne', 'afterFindOne',
11+
'beforeCount', 'afterCount',
12+
'beforeAggregate', 'afterAggregate',
13+
14+
// Write Operations
15+
'beforeInsert', 'afterInsert',
16+
'beforeUpdate', 'afterUpdate',
17+
'beforeDelete', 'afterDelete',
18+
]);
19+
20+
/**
21+
* Hook Definition Schema
22+
*
23+
* Hooks serve as the "Logic Layer" in ObjectStack, allowing developers to
24+
* inject custom code during the data access lifecycle.
25+
*
26+
* Use cases:
27+
* - Data Enrichment (Default values, Calculated fields)
28+
* - Validation (Complex business rules)
29+
* - Side Effects (Sending emails, Syncing to external systems)
30+
* - Security (Filtering data based on context)
31+
*/
32+
export const HookSchema = z.object({
33+
/**
34+
* Unique identifier for the hook
35+
* Required for debugging and overriding.
36+
*/
37+
name: z.string().regex(/^[a-z_][a-z0-9_]*$/).describe('Hook unique name (snake_case)'),
38+
39+
/**
40+
* Human readable label
41+
*/
42+
label: z.string().optional().describe('Description of what this hook does'),
43+
44+
/**
45+
* Target Object(s)
46+
* can be:
47+
* - Single object: "account"
48+
* - List of objects: ["account", "contact"]
49+
* - Wildcard: "*" (All objects)
50+
*/
51+
object: z.union([z.string(), z.array(z.string())]).describe('Target object(s)'),
52+
53+
/**
54+
* Events to subscribe to
55+
* Combinations of timing (before/after) and action (find/insert/update/delete/etc)
56+
*/
57+
events: z.array(HookEvent).describe('Lifecycle events'),
58+
59+
/**
60+
* Handler Logic
61+
* Reference to a registered function in the plugin system.
62+
*/
63+
handler: z.string().optional().describe('Function handler name (e.g. "my_plugin.validate_account")'),
64+
65+
/**
66+
* Inline Script (Optional)
67+
* For simple logic without a full plugin.
68+
* @deprecated Prefer 'handler' for better testability and type safety.
69+
*/
70+
script: z.string().optional().describe('Inline script body'),
71+
72+
/**
73+
* Execution Order
74+
* Lower numbers run first.
75+
* - System Hooks: 0-99
76+
* - App Hooks: 100-999
77+
* - User Hooks: 1000+
78+
*/
79+
priority: z.number().default(100).describe('Execution priority'),
80+
81+
/**
82+
* Async / Background Execution
83+
* If true, the hook runs in the background and does not block the transaction.
84+
* Only applicable for 'after*' events.
85+
* Default: false (Blocking)
86+
*/
87+
async: z.boolean().default(false).describe('Run specifically as fire-and-forget'),
88+
89+
/**
90+
* Error Policy
91+
* What to do if the hook throws an exception?
92+
* - abort: Rollback transaction (if blocking)
93+
* - log: Log error and continue
94+
*/
95+
onError: z.enum(['abort', 'log']).default('abort').describe('Error handling strategy'),
96+
});
97+
98+
export type Hook = z.infer<typeof HookSchema>;
99+
export type HookEventType = z.infer<typeof HookEvent>;

packages/spec/src/data/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,6 @@ export * from './filter.zod';
33
export * from './object.zod';
44
export * from './field.zod';
55
export * from './validation.zod';
6+
export * from './hook.zod';
7+
68
export * from './dataset.zod';

packages/spec/src/permission/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,3 @@
99

1010
export * from './permission.zod';
1111
export * from './sharing.zod';
12-
export * from './trigger.zod';

0 commit comments

Comments
 (0)