bunsane 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/deploy-docs.yml +57 -0
- package/LICENSE.md +1 -1
- package/README.md +2 -28
- package/TODO.md +8 -1
- package/bun.lock +3 -0
- package/config/upload.config.ts +135 -0
- package/core/App.ts +168 -4
- package/core/ArcheType.ts +122 -0
- package/core/BatchLoader.ts +100 -0
- package/core/ComponentRegistry.ts +4 -3
- package/core/Components.ts +2 -2
- package/core/Decorators.ts +15 -8
- package/core/Entity.ts +193 -14
- package/core/EntityCache.ts +15 -0
- package/core/EntityHookManager.ts +855 -0
- package/core/EntityManager.ts +12 -2
- package/core/ErrorHandler.ts +64 -7
- package/core/FileValidator.ts +284 -0
- package/core/Query.ts +503 -85
- package/core/RequestContext.ts +24 -0
- package/core/RequestLoaders.ts +89 -0
- package/core/SchedulerManager.ts +710 -0
- package/core/UploadManager.ts +261 -0
- package/core/components/UploadComponent.ts +206 -0
- package/core/decorators/EntityHooks.ts +190 -0
- package/core/decorators/ScheduledTask.ts +83 -0
- package/core/events/EntityLifecycleEvents.ts +177 -0
- package/core/processors/ImageProcessor.ts +423 -0
- package/core/storage/LocalStorageProvider.ts +290 -0
- package/core/storage/StorageProvider.ts +112 -0
- package/database/DatabaseHelper.ts +183 -58
- package/database/index.ts +5 -5
- package/database/sqlHelpers.ts +7 -0
- package/docs/README.md +149 -0
- package/docs/_coverpage.md +36 -0
- package/docs/_sidebar.md +23 -0
- package/docs/api/core.md +568 -0
- package/docs/api/hooks.md +554 -0
- package/docs/api/index.md +222 -0
- package/docs/api/query.md +678 -0
- package/docs/api/service.md +744 -0
- package/docs/core-concepts/archetypes.md +512 -0
- package/docs/core-concepts/components.md +498 -0
- package/docs/core-concepts/entity.md +314 -0
- package/docs/core-concepts/hooks.md +683 -0
- package/docs/core-concepts/query.md +588 -0
- package/docs/core-concepts/services.md +647 -0
- package/docs/examples/code-examples.md +425 -0
- package/docs/getting-started.md +337 -0
- package/docs/index.html +97 -0
- package/gql/Generator.ts +58 -35
- package/gql/decorators/Upload.ts +176 -0
- package/gql/helpers.ts +67 -0
- package/gql/index.ts +65 -31
- package/gql/types.ts +1 -1
- package/index.ts +79 -11
- package/package.json +19 -10
- package/rest/Generator.ts +3 -0
- package/rest/index.ts +22 -0
- package/service/Service.ts +1 -1
- package/service/ServiceRegistry.ts +10 -6
- package/service/index.ts +12 -1
- package/tests/bench/insert.bench.ts +59 -0
- package/tests/bench/relations.bench.ts +269 -0
- package/tests/bench/sorting.bench.ts +415 -0
- package/tests/component-hooks.test.ts +1409 -0
- package/tests/component.test.ts +338 -0
- package/tests/errorHandling.test.ts +155 -0
- package/tests/hooks.test.ts +666 -0
- package/tests/query-sorting.test.ts +101 -0
- package/tests/relations.test.ts +169 -0
- package/tests/scheduler.test.ts +724 -0
- package/tsconfig.json +35 -34
- package/types/graphql.types.ts +87 -0
- package/types/hooks.types.ts +141 -0
- package/types/scheduler.types.ts +165 -0
- package/types/upload.types.ts +184 -0
- package/upload/index.ts +140 -0
- package/utils/UploadHelper.ts +305 -0
- package/utils/cronParser.ts +366 -0
- package/utils/errorMessages.ts +151 -0
- package/core/Events.ts +0 -0
|
@@ -0,0 +1,683 @@
|
|
|
1
|
+
# Entity Lifecycle Hooks
|
|
2
|
+
|
|
3
|
+
Entity Lifecycle Hooks provide a powerful way to execute business logic at specific points during an entity's lifecycle. They enable you to react to entity creation, updates, and deletion, as well as component changes, making it easy to implement cross-cutting concerns like auditing, notifications, and data validation.
|
|
4
|
+
|
|
5
|
+
## 🎯 What are Lifecycle Hooks?
|
|
6
|
+
|
|
7
|
+
Lifecycle hooks are functions that automatically execute when specific events occur in an entity's lifecycle. They allow you to:
|
|
8
|
+
|
|
9
|
+
- **Validate data** before saving
|
|
10
|
+
- **Send notifications** when entities change
|
|
11
|
+
- **Update related entities** automatically
|
|
12
|
+
- **Log audit trails** for compliance
|
|
13
|
+
- **Enforce business rules** across your application
|
|
14
|
+
- **Trigger background tasks** based on entity changes
|
|
15
|
+
|
|
16
|
+
### Hook Types
|
|
17
|
+
|
|
18
|
+
- **Entity Hooks**: Triggered by entity lifecycle events (create, update, delete)
|
|
19
|
+
- **Component Hooks**: Triggered by component changes on entities
|
|
20
|
+
- **Component-Targeted Hooks**: Entity hooks with fine-grained component-based filtering
|
|
21
|
+
- **Lifecycle Hooks**: Hooks that listen to all lifecycle events
|
|
22
|
+
|
|
23
|
+
## 🏗️ Basic Hook Implementation
|
|
24
|
+
|
|
25
|
+
### Entity Lifecycle Hooks
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
import { EntityHook, registerDecoratedHooks } from 'bunsane/decorators/EntityHooks';
|
|
29
|
+
import { EntityCreatedEvent, EntityUpdatedEvent, EntityDeletedEvent } from 'bunsane/events/EntityLifecycleEvents';
|
|
30
|
+
|
|
31
|
+
export class UserService {
|
|
32
|
+
@EntityHook('entity.created')
|
|
33
|
+
async onUserCreated(event: EntityCreatedEvent) {
|
|
34
|
+
console.log('New user created:', event.getEntity().id);
|
|
35
|
+
|
|
36
|
+
// Send welcome email
|
|
37
|
+
await this.sendWelcomeEmail(event.getEntity());
|
|
38
|
+
|
|
39
|
+
// Create user stats
|
|
40
|
+
// Implementation here...
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@EntityHook('entity.updated')
|
|
44
|
+
async onUserUpdated(event: EntityUpdatedEvent) {
|
|
45
|
+
console.log('User updated:', event.getEntity().id);
|
|
46
|
+
|
|
47
|
+
// Log the change
|
|
48
|
+
await this.logUserChange(event);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
@EntityHook('entity.deleted')
|
|
52
|
+
async onUserDeleted(event: EntityDeletedEvent) {
|
|
53
|
+
console.log('User deleted:', event.getEntity().id);
|
|
54
|
+
|
|
55
|
+
// Clean up related data
|
|
56
|
+
await this.cleanupUserData(event.getEntity().id);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Register hooks when service is instantiated
|
|
60
|
+
constructor() {
|
|
61
|
+
registerDecoratedHooks(this);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Component Lifecycle Hooks
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
import { ComponentHook } from 'bunsane/decorators/EntityHooks';
|
|
70
|
+
import { ComponentAddedEvent, ComponentUpdatedEvent, ComponentRemovedEvent } from 'bunsane/events/EntityLifecycleEvents';
|
|
71
|
+
|
|
72
|
+
export class UserService {
|
|
73
|
+
@ComponentHook('component.added')
|
|
74
|
+
async onComponentAdded(event: ComponentAddedEvent) {
|
|
75
|
+
if (event.getComponentType() === 'EmailComponent') {
|
|
76
|
+
console.log('Email component added to entity:', event.getEntity().id);
|
|
77
|
+
|
|
78
|
+
// Validate email format
|
|
79
|
+
const emailComponent = event.getComponent();
|
|
80
|
+
if (!this.isValidEmail(emailComponent.value)) {
|
|
81
|
+
throw new Error('Invalid email format');
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
@ComponentHook('component.updated')
|
|
87
|
+
async onComponentUpdated(event: ComponentUpdatedEvent) {
|
|
88
|
+
if (event.getComponentType() === 'EmailComponent') {
|
|
89
|
+
console.log('Email component updated');
|
|
90
|
+
|
|
91
|
+
// Check for email changes
|
|
92
|
+
const oldEmail = event.getOldData()?.value;
|
|
93
|
+
const newEmail = event.getNewData()?.value;
|
|
94
|
+
|
|
95
|
+
if (oldEmail !== newEmail) {
|
|
96
|
+
// Send email verification
|
|
97
|
+
await this.sendEmailVerification(newEmail);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
@ComponentHook('component.removed')
|
|
103
|
+
async onComponentRemoved(event: ComponentRemovedEvent) {
|
|
104
|
+
if (event.getComponentType() === 'ProfileComponent') {
|
|
105
|
+
console.log('Profile component removed');
|
|
106
|
+
|
|
107
|
+
// Handle profile removal
|
|
108
|
+
await this.handleProfileRemoval(event.getEntity().id);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## 🎭 Component-Targeted Hooks
|
|
115
|
+
|
|
116
|
+
### Archetype-Targeted Hooks
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
import { ComponentTargetHook } from 'bunsane/decorators/EntityHooks';
|
|
120
|
+
|
|
121
|
+
export class ContentService {
|
|
122
|
+
@ComponentTargetHook('entity.created', {
|
|
123
|
+
includeComponents: [BlogPost, AuthorComponent]
|
|
124
|
+
})
|
|
125
|
+
async onBlogPostCreated(event: EntityCreatedEvent) {
|
|
126
|
+
console.log('New blog post created');
|
|
127
|
+
|
|
128
|
+
// Extract post data
|
|
129
|
+
const postData = await event.getEntity().get(BlogPost);
|
|
130
|
+
const authorData = await event.getEntity().get(AuthorComponent);
|
|
131
|
+
|
|
132
|
+
// Notify followers
|
|
133
|
+
await this.notifyFollowers(authorData.authorId, {
|
|
134
|
+
type: 'new_post',
|
|
135
|
+
postId: event.getEntity().id,
|
|
136
|
+
title: postData.title
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Update author stats
|
|
140
|
+
await this.incrementAuthorPostCount(authorData.authorId);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
@ComponentTargetHook('entity.updated', {
|
|
144
|
+
includeComponents: [PublishedStatus],
|
|
145
|
+
requireAllIncluded: true
|
|
146
|
+
})
|
|
147
|
+
async onContentPublished(event: EntityUpdatedEvent) {
|
|
148
|
+
const entity = event.getEntity();
|
|
149
|
+
|
|
150
|
+
// Check if this is a publish event
|
|
151
|
+
if (entity.has(PublishedStatus)) {
|
|
152
|
+
const status = await entity.get(PublishedStatus);
|
|
153
|
+
|
|
154
|
+
if (status.isPublished && !status.wasPublished) {
|
|
155
|
+
// Content was just published
|
|
156
|
+
await this.onContentPublished(entity);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Conditional Component Targeting
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
export class NotificationService {
|
|
167
|
+
@ComponentTargetHook('entity.created', {
|
|
168
|
+
includeComponents: [UserProfile],
|
|
169
|
+
excludeComponents: [GuestUser]
|
|
170
|
+
})
|
|
171
|
+
async onRegularUserCreated(event: EntityCreatedEvent) {
|
|
172
|
+
// Only triggered for regular users, not guests
|
|
173
|
+
await this.sendWelcomeEmail(event.getEntity());
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
@ComponentTargetHook('entity.updated', {
|
|
177
|
+
includeComponents: [UserProfile, EmailComponent],
|
|
178
|
+
requireAllIncluded: true
|
|
179
|
+
})
|
|
180
|
+
async onUserEmailChanged(event: EntityUpdatedEvent) {
|
|
181
|
+
// Only triggered when both UserProfile and EmailComponent are present
|
|
182
|
+
const profile = await event.getEntity().get(UserProfile);
|
|
183
|
+
const email = await event.getEntity().get(EmailComponent);
|
|
184
|
+
|
|
185
|
+
await this.sendEmailChangeNotification(profile.email, email.value);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## 🔧 Hook Configuration and Options
|
|
191
|
+
|
|
192
|
+
### Hook Priority and Execution Order
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
export class OrderedHookService {
|
|
196
|
+
@EntityHook('entity.created', { priority: 1 })
|
|
197
|
+
async validateEntity(event: EntityCreatedEvent) {
|
|
198
|
+
// High priority validation (executes first)
|
|
199
|
+
if (!this.isValidEntity(event.getEntity())) {
|
|
200
|
+
throw new Error('Entity validation failed');
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
@EntityHook('entity.created', { priority: 10 })
|
|
205
|
+
async sendWelcomeEmail(event: EntityCreatedEvent) {
|
|
206
|
+
// Lower priority - runs after validation
|
|
207
|
+
await this.sendEmail(event.getEntity());
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
@EntityHook('entity.created', { priority: 5 })
|
|
211
|
+
async createDefaultComponents(event: EntityCreatedEvent) {
|
|
212
|
+
// Medium priority
|
|
213
|
+
await this.addDefaultComponents(event.getEntity());
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Async Hooks and Timeouts
|
|
219
|
+
|
|
220
|
+
```typescript
|
|
221
|
+
export class AsyncHookService {
|
|
222
|
+
@EntityHook('entity.created', { async: true, timeout: 5000 })
|
|
223
|
+
async sendWelcomeEmailAsync(event: EntityCreatedEvent) {
|
|
224
|
+
// This hook runs asynchronously with 5 second timeout
|
|
225
|
+
try {
|
|
226
|
+
await this.sendWelcomeEmail(event.getEntity());
|
|
227
|
+
} catch (error) {
|
|
228
|
+
console.error('Failed to send welcome email:', error);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
@ComponentHook('component.updated', { timeout: 2000 })
|
|
233
|
+
async validateComponentUpdate(event: ComponentUpdatedEvent) {
|
|
234
|
+
// 2 second timeout for validation
|
|
235
|
+
const isValid = await this.validateUpdate(event);
|
|
236
|
+
if (!isValid) {
|
|
237
|
+
throw new Error('Component update validation failed');
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### Hook Filtering
|
|
244
|
+
|
|
245
|
+
```typescript
|
|
246
|
+
export class FilteredHookService {
|
|
247
|
+
@EntityHook('entity.created', {
|
|
248
|
+
filter: (event) => event.getEntity().has(PremiumFeature)
|
|
249
|
+
})
|
|
250
|
+
async onPremiumUserCreated(event: EntityCreatedEvent) {
|
|
251
|
+
// Only executes for premium users
|
|
252
|
+
await this.setupPremiumFeatures(event.getEntity());
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
@ComponentHook('component.updated', {
|
|
256
|
+
filter: (event) => {
|
|
257
|
+
const oldData = event.getOldData();
|
|
258
|
+
const newData = event.getNewData();
|
|
259
|
+
return oldData?.status !== newData?.status; // Only status changes
|
|
260
|
+
}
|
|
261
|
+
})
|
|
262
|
+
async onStatusChanged(event: ComponentUpdatedEvent) {
|
|
263
|
+
// Only executes when status actually changes
|
|
264
|
+
await this.handleStatusChange(event);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
## 🎯 Real-World Examples
|
|
270
|
+
|
|
271
|
+
### Audit Logging (from examples/hooks/audit-logger.ts)
|
|
272
|
+
|
|
273
|
+
```typescript
|
|
274
|
+
import { EntityHook, ComponentHook } from 'bunsane/decorators/EntityHooks';
|
|
275
|
+
|
|
276
|
+
export class AuditLogger {
|
|
277
|
+
@EntityHook("entity.created")
|
|
278
|
+
async handleEntityCreated(event: EntityCreatedEvent) {
|
|
279
|
+
const entry = {
|
|
280
|
+
id: this.generateId(),
|
|
281
|
+
timestamp: new Date(),
|
|
282
|
+
action: 'create',
|
|
283
|
+
entityId: event.getEntity().id,
|
|
284
|
+
entityType: this.getEntityType(event.getEntity()),
|
|
285
|
+
userId: this.getCurrentUserId(),
|
|
286
|
+
newData: await this.extractEntityData(event.getEntity())
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
await this.storeLogEntry(entry);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
@EntityHook("entity.updated")
|
|
293
|
+
async handleEntityUpdated(event: EntityUpdatedEvent) {
|
|
294
|
+
const entry = {
|
|
295
|
+
id: this.generateId(),
|
|
296
|
+
timestamp: new Date(),
|
|
297
|
+
action: 'update',
|
|
298
|
+
entityId: event.getEntity().id,
|
|
299
|
+
changedComponents: event.getChangedComponents()
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
await this.storeLogEntry(entry);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
@ComponentHook("component.added")
|
|
306
|
+
async handleComponentAdded(event: ComponentAddedEvent) {
|
|
307
|
+
const entry = {
|
|
308
|
+
id: this.generateId(),
|
|
309
|
+
action: 'add_component',
|
|
310
|
+
entityId: event.getEntity().id,
|
|
311
|
+
componentType: event.getComponentType(),
|
|
312
|
+
newData: event.getComponent().data()
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
await this.storeLogEntry(entry);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### User Service Hooks (from UserService.ts)
|
|
321
|
+
|
|
322
|
+
```typescript
|
|
323
|
+
export class UserService extends BaseService {
|
|
324
|
+
@ComponentTargetHook("entity.created", {
|
|
325
|
+
includeComponents: [UserTag, EmailComponent]
|
|
326
|
+
})
|
|
327
|
+
async onUserCreate(event: EntityCreatedEvent) {
|
|
328
|
+
const emailComp = await event.entity.get(EmailComponent);
|
|
329
|
+
logger.info(`New user created with email: ${emailComp?.value}`);
|
|
330
|
+
// Here you could add logic to send a welcome email, etc.
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
## 🔄 Hook Registration and Management
|
|
336
|
+
|
|
337
|
+
### Automatic Hook Registration
|
|
338
|
+
|
|
339
|
+
```typescript
|
|
340
|
+
import { registerDecoratedHooks } from 'bunsane/decorators/EntityHooks';
|
|
341
|
+
|
|
342
|
+
export class MyService {
|
|
343
|
+
@EntityHook('entity.created')
|
|
344
|
+
async handleCreation(event: EntityCreatedEvent) {
|
|
345
|
+
// Hook implementation
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
constructor() {
|
|
349
|
+
// Automatically register all decorated hooks
|
|
350
|
+
registerDecoratedHooks(this);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
### Manual Hook Registration
|
|
356
|
+
|
|
357
|
+
```typescript
|
|
358
|
+
import EntityHookManager from 'bunsane/core/EntityHookManager';
|
|
359
|
+
|
|
360
|
+
class CustomNotificationService {
|
|
361
|
+
async sendNotification(entityId: string, message: string) {
|
|
362
|
+
// Implementation
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
registerHooks() {
|
|
366
|
+
// Register entity hooks
|
|
367
|
+
EntityHookManager.registerEntityHook(
|
|
368
|
+
'entity.created',
|
|
369
|
+
async (event: EntityCreatedEvent) => {
|
|
370
|
+
if (event.getEntity().has(UserProfile)) {
|
|
371
|
+
await this.sendNotification(
|
|
372
|
+
event.getEntity().id,
|
|
373
|
+
'Welcome to our platform!'
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
},
|
|
377
|
+
{ priority: 5 }
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
// Register component hooks
|
|
381
|
+
EntityHookManager.registerComponentHook(
|
|
382
|
+
'component.updated',
|
|
383
|
+
async (event: ComponentUpdatedEvent) => {
|
|
384
|
+
if (event.getComponentType() === 'UserProfile') {
|
|
385
|
+
await this.sendNotification(
|
|
386
|
+
event.getEntity().id,
|
|
387
|
+
'Your profile has been updated'
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
## 🎯 Advanced Hook Patterns
|
|
397
|
+
|
|
398
|
+
### Batch Event Processing
|
|
399
|
+
|
|
400
|
+
```typescript
|
|
401
|
+
export class BatchProcessor {
|
|
402
|
+
private eventBuffer: LifecycleEvent[] = [];
|
|
403
|
+
private processingTimer: NodeJS.Timeout | null = null;
|
|
404
|
+
|
|
405
|
+
@EntityHook('entity.created', { async: true })
|
|
406
|
+
async bufferEvent(event: EntityCreatedEvent) {
|
|
407
|
+
this.eventBuffer.push(event);
|
|
408
|
+
|
|
409
|
+
// Process in batches of 10 or after 5 seconds
|
|
410
|
+
if (this.eventBuffer.length >= 10) {
|
|
411
|
+
await this.processBatch();
|
|
412
|
+
} else if (!this.processingTimer) {
|
|
413
|
+
this.processingTimer = setTimeout(() => this.processBatch(), 5000);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
private async processBatch() {
|
|
418
|
+
if (this.processingTimer) {
|
|
419
|
+
clearTimeout(this.processingTimer);
|
|
420
|
+
this.processingTimer = null;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const events = [...this.eventBuffer];
|
|
424
|
+
this.eventBuffer = [];
|
|
425
|
+
|
|
426
|
+
// Process events in batch
|
|
427
|
+
await this.bulkProcessEvents(events);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
### Saga Pattern with Hooks
|
|
433
|
+
|
|
434
|
+
```typescript
|
|
435
|
+
export class OrderSagaService {
|
|
436
|
+
private sagas = new Map<string, SagaState>();
|
|
437
|
+
|
|
438
|
+
@EntityHook('entity.created')
|
|
439
|
+
async startOrderSaga(event: EntityCreatedEvent) {
|
|
440
|
+
if (event.getEntity().has(OrderComponent)) {
|
|
441
|
+
const sagaId = `order-${event.getEntity().id}`;
|
|
442
|
+
|
|
443
|
+
this.sagas.set(sagaId, {
|
|
444
|
+
id: sagaId,
|
|
445
|
+
steps: ['validate', 'charge', 'ship', 'complete'],
|
|
446
|
+
currentStep: 0,
|
|
447
|
+
entityId: event.getEntity().id
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
await this.executeSagaStep(sagaId);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
@EntityHook('entity.updated')
|
|
455
|
+
async continueOrderSaga(event: EntityUpdatedEvent) {
|
|
456
|
+
const sagaId = `order-${event.getEntity().id}`;
|
|
457
|
+
const saga = this.sagas.get(sagaId);
|
|
458
|
+
|
|
459
|
+
if (saga && saga.currentStep < saga.steps.length) {
|
|
460
|
+
await this.executeSagaStep(sagaId);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
private async executeSagaStep(sagaId: string) {
|
|
465
|
+
const saga = this.sagas.get(sagaId);
|
|
466
|
+
if (!saga) return;
|
|
467
|
+
|
|
468
|
+
const step = saga.steps[saga.currentStep];
|
|
469
|
+
try {
|
|
470
|
+
await this.executeStep(step, saga.entityId);
|
|
471
|
+
saga.currentStep++;
|
|
472
|
+
|
|
473
|
+
if (saga.currentStep >= saga.steps.length) {
|
|
474
|
+
// Saga completed
|
|
475
|
+
this.sagas.delete(sagaId);
|
|
476
|
+
}
|
|
477
|
+
} catch (error) {
|
|
478
|
+
// Saga failed - execute compensation
|
|
479
|
+
await this.compensateSaga(saga);
|
|
480
|
+
this.sagas.delete(sagaId);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
## ⚡ Performance Optimization
|
|
487
|
+
|
|
488
|
+
### Efficient Hook Execution
|
|
489
|
+
|
|
490
|
+
```typescript
|
|
491
|
+
export class EfficientHookService {
|
|
492
|
+
private cache = new Map<string, any>();
|
|
493
|
+
|
|
494
|
+
@EntityHook('entity.created')
|
|
495
|
+
async onEntityCreated(event: EntityCreatedEvent) {
|
|
496
|
+
// Cache expensive operations
|
|
497
|
+
const cacheKey = `entity:${event.getEntity().id}`;
|
|
498
|
+
if (this.cache.has(cacheKey)) {
|
|
499
|
+
return; // Already processed
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Perform expensive operation
|
|
503
|
+
await this.expensiveOperation(event.getEntity());
|
|
504
|
+
|
|
505
|
+
// Cache result
|
|
506
|
+
this.cache.set(cacheKey, true);
|
|
507
|
+
|
|
508
|
+
// Clean up cache periodically
|
|
509
|
+
if (this.cache.size > 1000) {
|
|
510
|
+
this.clearOldCacheEntries();
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
@ComponentTargetHook('entity.updated', {
|
|
515
|
+
includeComponents: [FrequentlyUpdatedComponent],
|
|
516
|
+
requireAllIncluded: true
|
|
517
|
+
})
|
|
518
|
+
async onFrequentUpdate(event: EntityUpdatedEvent) {
|
|
519
|
+
// Use component targeting to avoid unnecessary executions
|
|
520
|
+
await this.handleFrequentUpdate(event.getEntity());
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
### Hook Metrics and Monitoring
|
|
526
|
+
|
|
527
|
+
```typescript
|
|
528
|
+
export class MonitoredHookService {
|
|
529
|
+
private metrics = new Map<string, number[]>();
|
|
530
|
+
|
|
531
|
+
@EntityHook('entity.created')
|
|
532
|
+
async monitoredHook(event: EntityCreatedEvent) {
|
|
533
|
+
const startTime = performance.now();
|
|
534
|
+
|
|
535
|
+
try {
|
|
536
|
+
await this.doWork(event);
|
|
537
|
+
} finally {
|
|
538
|
+
const duration = performance.now() - startTime;
|
|
539
|
+
this.recordMetric('entity.created', duration);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
private recordMetric(hookName: string, duration: number) {
|
|
544
|
+
if (!this.metrics.has(hookName)) {
|
|
545
|
+
this.metrics.set(hookName, []);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const timings = this.metrics.get(hookName)!;
|
|
549
|
+
timings.push(duration);
|
|
550
|
+
|
|
551
|
+
// Keep only last 100 measurements
|
|
552
|
+
if (timings.length > 100) {
|
|
553
|
+
timings.shift();
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
getMetrics() {
|
|
558
|
+
const result: Record<string, any> = {};
|
|
559
|
+
|
|
560
|
+
for (const [hookName, timings] of this.metrics) {
|
|
561
|
+
const avg = timings.reduce((a, b) => a + b, 0) / timings.length;
|
|
562
|
+
const max = Math.max(...timings);
|
|
563
|
+
const min = Math.min(...timings);
|
|
564
|
+
|
|
565
|
+
result[hookName] = { avg, max, min, count: timings.length };
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
return result;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
## 🔧 Best Practices
|
|
574
|
+
|
|
575
|
+
### Error Handling in Hooks
|
|
576
|
+
|
|
577
|
+
```typescript
|
|
578
|
+
export class RobustHookService {
|
|
579
|
+
@EntityHook('entity.created')
|
|
580
|
+
async onEntityCreated(event: EntityCreatedEvent) {
|
|
581
|
+
try {
|
|
582
|
+
await this.processEntityCreation(event);
|
|
583
|
+
} catch (error) {
|
|
584
|
+
// Log error but don't prevent entity creation
|
|
585
|
+
console.error('Hook processing failed:', error);
|
|
586
|
+
|
|
587
|
+
// Optionally send to error tracking service
|
|
588
|
+
await this.reportError(error, {
|
|
589
|
+
hook: 'entity.created',
|
|
590
|
+
entityId: event.getEntity().id
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
@EntityHook('entity.created')
|
|
596
|
+
async criticalValidation(event: EntityCreatedEvent) {
|
|
597
|
+
// For critical validations, let errors propagate
|
|
598
|
+
// This will prevent entity creation if validation fails
|
|
599
|
+
if (!this.isValidEntity(event.getEntity())) {
|
|
600
|
+
throw new Error('Critical validation failed - entity creation blocked');
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
### Hook Testing
|
|
607
|
+
|
|
608
|
+
```typescript
|
|
609
|
+
import { describe, test, expect, beforeEach } from 'bun:test';
|
|
610
|
+
import { Entity, EntityCreatedEvent } from 'bunsane';
|
|
611
|
+
|
|
612
|
+
describe('User Creation Hooks', () => {
|
|
613
|
+
let hookService: UserService;
|
|
614
|
+
let mockEmailService: any;
|
|
615
|
+
|
|
616
|
+
beforeEach(() => {
|
|
617
|
+
mockEmailService = {
|
|
618
|
+
sendWelcomeEmail: jest.fn(),
|
|
619
|
+
sendVerificationEmail: jest.fn()
|
|
620
|
+
};
|
|
621
|
+
|
|
622
|
+
hookService = new UserService(mockEmailService);
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
test('should send welcome email on user creation', async () => {
|
|
626
|
+
// Create test entity
|
|
627
|
+
const userEntity = UserArcheType.fill({
|
|
628
|
+
userProfile: { name: 'Test User', email: 'test@example.com' }
|
|
629
|
+
}).createEntity();
|
|
630
|
+
|
|
631
|
+
// Create event
|
|
632
|
+
const event = new EntityCreatedEvent(userEntity);
|
|
633
|
+
|
|
634
|
+
// Trigger hook manually for testing
|
|
635
|
+
await hookService.onUserCreated(event);
|
|
636
|
+
|
|
637
|
+
// Verify email was sent
|
|
638
|
+
expect(mockEmailService.sendWelcomeEmail).toHaveBeenCalledWith(
|
|
639
|
+
'test@example.com'
|
|
640
|
+
);
|
|
641
|
+
});
|
|
642
|
+
});
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
## 🚀 Event Types Reference
|
|
646
|
+
|
|
647
|
+
### Entity Events
|
|
648
|
+
|
|
649
|
+
- **`entity.created`**: Fired when an entity is created (first save)
|
|
650
|
+
- **`entity.updated`**: Fired when an entity is updated (subsequent saves)
|
|
651
|
+
- **`entity.deleted`**: Fired when an entity is deleted
|
|
652
|
+
|
|
653
|
+
### Component Events
|
|
654
|
+
|
|
655
|
+
- **`component.added`**: Fired when a component is added to an entity
|
|
656
|
+
- **`component.updated`**: Fired when a component data is updated
|
|
657
|
+
- **`component.removed`**: Fired when a component is removed from an entity
|
|
658
|
+
|
|
659
|
+
### Event Properties
|
|
660
|
+
|
|
661
|
+
All events provide:
|
|
662
|
+
- `eventType`: String identifier of the event type
|
|
663
|
+
- `timestamp`: When the event occurred
|
|
664
|
+
- `entity`: The entity associated with the event
|
|
665
|
+
|
|
666
|
+
Entity events additionally provide:
|
|
667
|
+
- `isNew`: Boolean indicating if this is a new entity
|
|
668
|
+
|
|
669
|
+
Component events additionally provide:
|
|
670
|
+
- `component`: The component instance
|
|
671
|
+
- `componentType`: String identifier of the component type
|
|
672
|
+
|
|
673
|
+
## 🚀 What's Next?
|
|
674
|
+
|
|
675
|
+
Now that you understand Lifecycle Hooks, let's explore:
|
|
676
|
+
|
|
677
|
+
- **[Services](services.md)** - Using hooks in services
|
|
678
|
+
- **[Query System](query.md)** - Advanced querying with hooks
|
|
679
|
+
- **[API Reference](../api/)** - Complete hook API documentation
|
|
680
|
+
|
|
681
|
+
---
|
|
682
|
+
|
|
683
|
+
*Ready to add dynamic behavior to your entities? Let's look at [Advanced Features](../advanced/) next!* 🚀
|