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,1409 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, afterEach, beforeAll } from "bun:test";
|
|
2
|
+
import { Entity } from "../core/Entity";
|
|
3
|
+
import hookManager from "../core/EntityHookManager";
|
|
4
|
+
import { EntityCreatedEvent, EntityUpdatedEvent, EntityDeletedEvent } from "../core/events/EntityLifecycleEvents";
|
|
5
|
+
import { BaseComponent, CompData, Component } from "../core/Components";
|
|
6
|
+
import App from "../core/App";
|
|
7
|
+
import { ComponentTargetHook, registerDecoratedHooks } from "../core/decorators/EntityHooks";
|
|
8
|
+
import ArcheType from "../core/ArcheType";
|
|
9
|
+
|
|
10
|
+
let app: App;
|
|
11
|
+
|
|
12
|
+
// Test components
|
|
13
|
+
@Component
|
|
14
|
+
class UserTag extends BaseComponent {
|
|
15
|
+
@CompData()
|
|
16
|
+
userType: string = "regular";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
@Component
|
|
20
|
+
class AdminTag extends BaseComponent {
|
|
21
|
+
@CompData()
|
|
22
|
+
adminLevel: number = 1;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@Component
|
|
26
|
+
class TemporaryTag extends BaseComponent {
|
|
27
|
+
@CompData()
|
|
28
|
+
expiresAt: string = "";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@Component
|
|
32
|
+
class PostTag extends BaseComponent {
|
|
33
|
+
@CompData()
|
|
34
|
+
category: string = "general";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Archetype test components
|
|
38
|
+
@Component
|
|
39
|
+
class NameComponent extends BaseComponent {
|
|
40
|
+
@CompData()
|
|
41
|
+
value: string = "";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@Component
|
|
45
|
+
class EmailComponent extends BaseComponent {
|
|
46
|
+
@CompData()
|
|
47
|
+
value: string = "";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@Component
|
|
51
|
+
class AddressComponent extends BaseComponent {
|
|
52
|
+
@CompData()
|
|
53
|
+
street: string = "";
|
|
54
|
+
city: string = "";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Define archetypes for testing
|
|
58
|
+
const UserArchetype = new ArcheType([UserTag, NameComponent, EmailComponent]);
|
|
59
|
+
const AdminArchetype = new ArcheType([AdminTag, NameComponent, EmailComponent]);
|
|
60
|
+
const PostArchetype = new ArcheType([PostTag]);
|
|
61
|
+
|
|
62
|
+
beforeAll(async () => {
|
|
63
|
+
app = new App();
|
|
64
|
+
await app.waitForAppReady();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('Component-Specific Hook Targeting - Phase 1', () => {
|
|
68
|
+
beforeEach(() => {
|
|
69
|
+
hookManager.clearAllHooks();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
afterEach(() => {
|
|
73
|
+
hookManager.clearAllHooks();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('should execute hook only for entities with specific included component', async () => {
|
|
77
|
+
let hookExecuted = false;
|
|
78
|
+
let executedEntityId: string = "";
|
|
79
|
+
|
|
80
|
+
// Register hook that only executes for entities with UserTag
|
|
81
|
+
const hookId = hookManager.registerEntityHook("entity.created",
|
|
82
|
+
(event: EntityCreatedEvent) => {
|
|
83
|
+
hookExecuted = true;
|
|
84
|
+
executedEntityId = event.getEntity().id;
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
componentTarget: {
|
|
88
|
+
includeComponents: [UserTag]
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
expect(hookId).toBeDefined();
|
|
94
|
+
|
|
95
|
+
// Create entity with UserTag - hook should execute
|
|
96
|
+
const userEntity = Entity.Create();
|
|
97
|
+
userEntity.add(UserTag, { userType: "premium" });
|
|
98
|
+
await userEntity.save();
|
|
99
|
+
|
|
100
|
+
expect(hookExecuted).toBe(true);
|
|
101
|
+
expect(executedEntityId).toBe(userEntity.id);
|
|
102
|
+
|
|
103
|
+
// Reset for next test
|
|
104
|
+
hookExecuted = false;
|
|
105
|
+
executedEntityId = "";
|
|
106
|
+
|
|
107
|
+
// Create entity with different component - hook should NOT execute
|
|
108
|
+
const postEntity = Entity.Create();
|
|
109
|
+
postEntity.add(PostTag, { category: "news" });
|
|
110
|
+
await postEntity.save();
|
|
111
|
+
|
|
112
|
+
expect(hookExecuted).toBe(false);
|
|
113
|
+
expect(executedEntityId).toBe("");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('should execute hook only for entities without specific excluded component', async () => {
|
|
117
|
+
let hookExecuted = false;
|
|
118
|
+
let executedEntityId: string = "";
|
|
119
|
+
|
|
120
|
+
// Register hook that executes for entities WITHOUT TemporaryTag
|
|
121
|
+
const hookId = hookManager.registerEntityHook("entity.created",
|
|
122
|
+
(event: EntityCreatedEvent) => {
|
|
123
|
+
hookExecuted = true;
|
|
124
|
+
executedEntityId = event.getEntity().id;
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
componentTarget: {
|
|
128
|
+
excludeComponents: [TemporaryTag]
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
expect(hookId).toBeDefined();
|
|
134
|
+
|
|
135
|
+
// Create entity without TemporaryTag - hook should execute
|
|
136
|
+
const userEntity = Entity.Create();
|
|
137
|
+
userEntity.add(UserTag, { userType: "regular" });
|
|
138
|
+
await userEntity.save();
|
|
139
|
+
|
|
140
|
+
expect(hookExecuted).toBe(true);
|
|
141
|
+
expect(executedEntityId).toBe(userEntity.id);
|
|
142
|
+
|
|
143
|
+
// Reset for next test
|
|
144
|
+
hookExecuted = false;
|
|
145
|
+
executedEntityId = "";
|
|
146
|
+
|
|
147
|
+
// Create entity with TemporaryTag - hook should NOT execute
|
|
148
|
+
const tempEntity = Entity.Create();
|
|
149
|
+
tempEntity.add(UserTag, { userType: "temp" });
|
|
150
|
+
tempEntity.add(TemporaryTag, { expiresAt: "2025-12-31" });
|
|
151
|
+
await tempEntity.save();
|
|
152
|
+
|
|
153
|
+
expect(hookExecuted).toBe(false);
|
|
154
|
+
expect(executedEntityId).toBe("");
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('should execute hook with both include and exclude component targeting', async () => {
|
|
158
|
+
let hookExecuted = false;
|
|
159
|
+
let executedEntityId: string = "";
|
|
160
|
+
|
|
161
|
+
// Register hook for entities with UserTag but WITHOUT TemporaryTag
|
|
162
|
+
const hookId = hookManager.registerEntityHook("entity.created",
|
|
163
|
+
(event: EntityCreatedEvent) => {
|
|
164
|
+
hookExecuted = true;
|
|
165
|
+
executedEntityId = event.getEntity().id;
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
componentTarget: {
|
|
169
|
+
includeComponents: [UserTag],
|
|
170
|
+
excludeComponents: [TemporaryTag]
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
expect(hookId).toBeDefined();
|
|
176
|
+
|
|
177
|
+
// Create entity with UserTag but no TemporaryTag - hook should execute
|
|
178
|
+
const userEntity = Entity.Create();
|
|
179
|
+
userEntity.add(UserTag, { userType: "regular" });
|
|
180
|
+
await userEntity.save();
|
|
181
|
+
|
|
182
|
+
expect(hookExecuted).toBe(true);
|
|
183
|
+
expect(executedEntityId).toBe(userEntity.id);
|
|
184
|
+
|
|
185
|
+
// Reset for next test
|
|
186
|
+
hookExecuted = false;
|
|
187
|
+
executedEntityId = "";
|
|
188
|
+
|
|
189
|
+
// Create entity with UserTag AND TemporaryTag - hook should NOT execute
|
|
190
|
+
const tempUserEntity = Entity.Create();
|
|
191
|
+
tempUserEntity.add(UserTag, { userType: "temp" });
|
|
192
|
+
tempUserEntity.add(TemporaryTag, { expiresAt: "2025-12-31" });
|
|
193
|
+
await tempUserEntity.save();
|
|
194
|
+
|
|
195
|
+
expect(hookExecuted).toBe(false);
|
|
196
|
+
expect(executedEntityId).toBe("");
|
|
197
|
+
|
|
198
|
+
// Reset for next test
|
|
199
|
+
hookExecuted = false;
|
|
200
|
+
executedEntityId = "";
|
|
201
|
+
|
|
202
|
+
// Create entity with only TemporaryTag - hook should NOT execute
|
|
203
|
+
const tempEntity = Entity.Create();
|
|
204
|
+
tempEntity.add(TemporaryTag, { expiresAt: "2025-12-31" });
|
|
205
|
+
await tempEntity.save();
|
|
206
|
+
|
|
207
|
+
expect(hookExecuted).toBe(false);
|
|
208
|
+
expect(executedEntityId).toBe("");
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test('should execute hook with OR logic for multiple included components', async () => {
|
|
212
|
+
let hookExecuted = false;
|
|
213
|
+
let executedEntityId: string = "";
|
|
214
|
+
|
|
215
|
+
// Register hook for entities with UserTag OR AdminTag (requireAllIncluded = false)
|
|
216
|
+
const hookId = hookManager.registerEntityHook("entity.created",
|
|
217
|
+
(event: EntityCreatedEvent) => {
|
|
218
|
+
hookExecuted = true;
|
|
219
|
+
executedEntityId = event.getEntity().id;
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
componentTarget: {
|
|
223
|
+
includeComponents: [UserTag, AdminTag],
|
|
224
|
+
requireAllIncluded: false // OR logic
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
expect(hookId).toBeDefined();
|
|
230
|
+
|
|
231
|
+
// Create entity with UserTag - hook should execute
|
|
232
|
+
const userEntity = Entity.Create();
|
|
233
|
+
userEntity.add(UserTag, { userType: "regular" });
|
|
234
|
+
await userEntity.save();
|
|
235
|
+
|
|
236
|
+
expect(hookExecuted).toBe(true);
|
|
237
|
+
expect(executedEntityId).toBe(userEntity.id);
|
|
238
|
+
|
|
239
|
+
// Reset for next test
|
|
240
|
+
hookExecuted = false;
|
|
241
|
+
executedEntityId = "";
|
|
242
|
+
|
|
243
|
+
// Create entity with AdminTag - hook should execute
|
|
244
|
+
const adminEntity = Entity.Create();
|
|
245
|
+
adminEntity.add(AdminTag, { adminLevel: 2 });
|
|
246
|
+
await adminEntity.save();
|
|
247
|
+
|
|
248
|
+
expect(hookExecuted).toBe(true);
|
|
249
|
+
expect(executedEntityId).toBe(adminEntity.id);
|
|
250
|
+
|
|
251
|
+
// Reset for next test
|
|
252
|
+
hookExecuted = false;
|
|
253
|
+
executedEntityId = "";
|
|
254
|
+
|
|
255
|
+
// Create entity with neither component - hook should NOT execute
|
|
256
|
+
const postEntity = Entity.Create();
|
|
257
|
+
postEntity.add(PostTag, { category: "news" });
|
|
258
|
+
await postEntity.save();
|
|
259
|
+
|
|
260
|
+
expect(hookExecuted).toBe(false);
|
|
261
|
+
expect(executedEntityId).toBe("");
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test('should execute hook with AND logic for multiple included components', async () => {
|
|
265
|
+
let hookExecuted = false;
|
|
266
|
+
let executedEntityId: string = "";
|
|
267
|
+
|
|
268
|
+
// Register hook for entities with BOTH UserTag AND AdminTag (requireAllIncluded = true, default)
|
|
269
|
+
const hookId = hookManager.registerEntityHook("entity.created",
|
|
270
|
+
(event: EntityCreatedEvent) => {
|
|
271
|
+
hookExecuted = true;
|
|
272
|
+
executedEntityId = event.getEntity().id;
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
componentTarget: {
|
|
276
|
+
includeComponents: [UserTag, AdminTag],
|
|
277
|
+
requireAllIncluded: true // AND logic (default)
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
expect(hookId).toBeDefined();
|
|
283
|
+
|
|
284
|
+
// Create entity with both UserTag and AdminTag - hook should execute
|
|
285
|
+
const superUserEntity = Entity.Create();
|
|
286
|
+
superUserEntity.add(UserTag, { userType: "admin" });
|
|
287
|
+
superUserEntity.add(AdminTag, { adminLevel: 3 });
|
|
288
|
+
await superUserEntity.save();
|
|
289
|
+
|
|
290
|
+
expect(hookExecuted).toBe(true);
|
|
291
|
+
expect(executedEntityId).toBe(superUserEntity.id);
|
|
292
|
+
|
|
293
|
+
// Reset for next test
|
|
294
|
+
hookExecuted = false;
|
|
295
|
+
executedEntityId = "";
|
|
296
|
+
|
|
297
|
+
// Create entity with only UserTag - hook should NOT execute
|
|
298
|
+
const userEntity = Entity.Create();
|
|
299
|
+
userEntity.add(UserTag, { userType: "regular" });
|
|
300
|
+
await userEntity.save();
|
|
301
|
+
|
|
302
|
+
expect(hookExecuted).toBe(false);
|
|
303
|
+
expect(executedEntityId).toBe("");
|
|
304
|
+
|
|
305
|
+
// Reset for next test
|
|
306
|
+
hookExecuted = false;
|
|
307
|
+
executedEntityId = "";
|
|
308
|
+
|
|
309
|
+
// Create entity with only AdminTag - hook should NOT execute
|
|
310
|
+
const adminEntity = Entity.Create();
|
|
311
|
+
adminEntity.add(AdminTag, { adminLevel: 2 });
|
|
312
|
+
await adminEntity.save();
|
|
313
|
+
|
|
314
|
+
expect(hookExecuted).toBe(false);
|
|
315
|
+
expect(executedEntityId).toBe("");
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
test('should execute hook without component targeting (backward compatibility)', async () => {
|
|
319
|
+
let hookExecuted = false;
|
|
320
|
+
let executedEntityId: string = "";
|
|
321
|
+
|
|
322
|
+
// Register hook without component targeting (should work for all entities)
|
|
323
|
+
const hookId = hookManager.registerEntityHook("entity.created",
|
|
324
|
+
(event: EntityCreatedEvent) => {
|
|
325
|
+
hookExecuted = true;
|
|
326
|
+
executedEntityId = event.getEntity().id;
|
|
327
|
+
}
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
expect(hookId).toBeDefined();
|
|
331
|
+
|
|
332
|
+
// Create entity with UserTag - hook should execute
|
|
333
|
+
const userEntity = Entity.Create();
|
|
334
|
+
userEntity.add(UserTag, { userType: "regular" });
|
|
335
|
+
await userEntity.save();
|
|
336
|
+
|
|
337
|
+
expect(hookExecuted).toBe(true);
|
|
338
|
+
expect(executedEntityId).toBe(userEntity.id);
|
|
339
|
+
|
|
340
|
+
// Reset for next test
|
|
341
|
+
hookExecuted = false;
|
|
342
|
+
executedEntityId = "";
|
|
343
|
+
|
|
344
|
+
// Create entity with different component - hook should still execute
|
|
345
|
+
const postEntity = Entity.Create();
|
|
346
|
+
postEntity.add(PostTag, { category: "news" });
|
|
347
|
+
await postEntity.save();
|
|
348
|
+
|
|
349
|
+
expect(hookExecuted).toBe(true);
|
|
350
|
+
expect(executedEntityId).toBe(postEntity.id);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
test('should work with entity update events and component targeting', async () => {
|
|
354
|
+
let hookExecuted = false;
|
|
355
|
+
let executedEntityId: string = "";
|
|
356
|
+
|
|
357
|
+
// Register hook for entity updates on entities with UserTag
|
|
358
|
+
const hookId = hookManager.registerEntityHook("entity.updated",
|
|
359
|
+
(event: EntityUpdatedEvent) => {
|
|
360
|
+
hookExecuted = true;
|
|
361
|
+
executedEntityId = event.getEntity().id;
|
|
362
|
+
},
|
|
363
|
+
{
|
|
364
|
+
componentTarget: {
|
|
365
|
+
includeComponents: [UserTag]
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
expect(hookId).toBeDefined();
|
|
371
|
+
|
|
372
|
+
// Create and save entity first
|
|
373
|
+
const userEntity = Entity.Create();
|
|
374
|
+
userEntity.add(UserTag, { userType: "regular" });
|
|
375
|
+
await userEntity.save();
|
|
376
|
+
|
|
377
|
+
// Reset for update test
|
|
378
|
+
hookExecuted = false;
|
|
379
|
+
executedEntityId = "";
|
|
380
|
+
|
|
381
|
+
// Update the entity - hook should execute
|
|
382
|
+
await userEntity.set(UserTag, { userType: "premium" });
|
|
383
|
+
await userEntity.save();
|
|
384
|
+
|
|
385
|
+
expect(hookExecuted).toBe(true);
|
|
386
|
+
expect(executedEntityId).toBe(userEntity.id);
|
|
387
|
+
|
|
388
|
+
// Reset for next test
|
|
389
|
+
hookExecuted = false;
|
|
390
|
+
executedEntityId = "";
|
|
391
|
+
|
|
392
|
+
// Create and update entity without UserTag - hook should NOT execute
|
|
393
|
+
const postEntity = Entity.Create();
|
|
394
|
+
postEntity.add(PostTag, { category: "news" });
|
|
395
|
+
await postEntity.save();
|
|
396
|
+
|
|
397
|
+
await postEntity.set(PostTag, { category: "breaking" });
|
|
398
|
+
await postEntity.save();
|
|
399
|
+
|
|
400
|
+
expect(hookExecuted).toBe(false);
|
|
401
|
+
expect(executedEntityId).toBe("");
|
|
402
|
+
});
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
describe('ComponentTargetHook Decorator - Phase 2', () => {
|
|
406
|
+
beforeEach(() => {
|
|
407
|
+
hookManager.clearAllHooks();
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
afterEach(() => {
|
|
411
|
+
hookManager.clearAllHooks();
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
test('should register and execute decorated hook with include components', async () => {
|
|
415
|
+
let hookExecuted = false;
|
|
416
|
+
let capturedEntityId: string = "";
|
|
417
|
+
|
|
418
|
+
class TestUserService {
|
|
419
|
+
@ComponentTargetHook("entity.created", {
|
|
420
|
+
includeComponents: [UserTag]
|
|
421
|
+
})
|
|
422
|
+
async handleUserCreated(event: EntityCreatedEvent) {
|
|
423
|
+
hookExecuted = true;
|
|
424
|
+
capturedEntityId = event.getEntity().id;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const service = new TestUserService();
|
|
429
|
+
registerDecoratedHooks(service);
|
|
430
|
+
|
|
431
|
+
// Create entity with UserTag - hook should execute
|
|
432
|
+
const userEntity = Entity.Create();
|
|
433
|
+
userEntity.add(UserTag, { userType: "premium" });
|
|
434
|
+
await userEntity.save();
|
|
435
|
+
|
|
436
|
+
expect(hookExecuted).toBe(true);
|
|
437
|
+
expect(capturedEntityId).toBe(userEntity.id);
|
|
438
|
+
|
|
439
|
+
// Reset for next test
|
|
440
|
+
hookExecuted = false;
|
|
441
|
+
capturedEntityId = "";
|
|
442
|
+
|
|
443
|
+
// Create entity with different component - hook should NOT execute
|
|
444
|
+
const postEntity = Entity.Create();
|
|
445
|
+
postEntity.add(PostTag, { category: "news" });
|
|
446
|
+
await postEntity.save();
|
|
447
|
+
|
|
448
|
+
expect(hookExecuted).toBe(false);
|
|
449
|
+
expect(capturedEntityId).toBe("");
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
test('should register and execute decorated hook with exclude components', async () => {
|
|
453
|
+
let hookExecuted = false;
|
|
454
|
+
let capturedEntityId: string = "";
|
|
455
|
+
|
|
456
|
+
class TestUserService {
|
|
457
|
+
@ComponentTargetHook("entity.created", {
|
|
458
|
+
excludeComponents: [TemporaryTag]
|
|
459
|
+
})
|
|
460
|
+
async handleNonTemporaryEntity(event: EntityCreatedEvent) {
|
|
461
|
+
hookExecuted = true;
|
|
462
|
+
capturedEntityId = event.getEntity().id;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const service = new TestUserService();
|
|
467
|
+
registerDecoratedHooks(service);
|
|
468
|
+
|
|
469
|
+
// Create entity without TemporaryTag - hook should execute
|
|
470
|
+
const userEntity = Entity.Create();
|
|
471
|
+
userEntity.add(UserTag, { userType: "regular" });
|
|
472
|
+
await userEntity.save();
|
|
473
|
+
|
|
474
|
+
expect(hookExecuted).toBe(true);
|
|
475
|
+
expect(capturedEntityId).toBe(userEntity.id);
|
|
476
|
+
|
|
477
|
+
// Reset for next test
|
|
478
|
+
hookExecuted = false;
|
|
479
|
+
capturedEntityId = "";
|
|
480
|
+
|
|
481
|
+
// Create entity with TemporaryTag - hook should NOT execute
|
|
482
|
+
const tempEntity = Entity.Create();
|
|
483
|
+
tempEntity.add(UserTag, { userType: "temp" });
|
|
484
|
+
tempEntity.add(TemporaryTag, { expiresAt: "2025-12-31" });
|
|
485
|
+
await tempEntity.save();
|
|
486
|
+
|
|
487
|
+
expect(hookExecuted).toBe(false);
|
|
488
|
+
expect(capturedEntityId).toBe("");
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
test('should register and execute decorated hook with combined include and exclude', async () => {
|
|
492
|
+
let hookExecuted = false;
|
|
493
|
+
let capturedEntityId: string = "";
|
|
494
|
+
|
|
495
|
+
class TestUserService {
|
|
496
|
+
@ComponentTargetHook("entity.created", {
|
|
497
|
+
includeComponents: [UserTag],
|
|
498
|
+
excludeComponents: [TemporaryTag]
|
|
499
|
+
})
|
|
500
|
+
async handlePermanentUser(event: EntityCreatedEvent) {
|
|
501
|
+
hookExecuted = true;
|
|
502
|
+
capturedEntityId = event.getEntity().id;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const service = new TestUserService();
|
|
507
|
+
registerDecoratedHooks(service);
|
|
508
|
+
|
|
509
|
+
// Create entity with UserTag but no TemporaryTag - hook should execute
|
|
510
|
+
const userEntity = Entity.Create();
|
|
511
|
+
userEntity.add(UserTag, { userType: "regular" });
|
|
512
|
+
await userEntity.save();
|
|
513
|
+
|
|
514
|
+
expect(hookExecuted).toBe(true);
|
|
515
|
+
expect(capturedEntityId).toBe(userEntity.id);
|
|
516
|
+
|
|
517
|
+
// Reset for next test
|
|
518
|
+
hookExecuted = false;
|
|
519
|
+
capturedEntityId = "";
|
|
520
|
+
|
|
521
|
+
// Create entity with UserTag AND TemporaryTag - hook should NOT execute
|
|
522
|
+
const tempUserEntity = Entity.Create();
|
|
523
|
+
tempUserEntity.add(UserTag, { userType: "temp" });
|
|
524
|
+
tempUserEntity.add(TemporaryTag, { expiresAt: "2025-12-31" });
|
|
525
|
+
await tempUserEntity.save();
|
|
526
|
+
|
|
527
|
+
expect(hookExecuted).toBe(false);
|
|
528
|
+
expect(capturedEntityId).toBe("");
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
test('should register and execute decorated hook with OR logic', async () => {
|
|
532
|
+
let hookExecuted = false;
|
|
533
|
+
let capturedEntityId: string = "";
|
|
534
|
+
|
|
535
|
+
class TestMultiComponentService {
|
|
536
|
+
@ComponentTargetHook("entity.created", {
|
|
537
|
+
includeComponents: [UserTag, AdminTag],
|
|
538
|
+
requireAllIncluded: false // OR logic
|
|
539
|
+
})
|
|
540
|
+
async handleUserOrAdmin(event: EntityCreatedEvent) {
|
|
541
|
+
hookExecuted = true;
|
|
542
|
+
capturedEntityId = event.getEntity().id;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
const service = new TestMultiComponentService();
|
|
547
|
+
registerDecoratedHooks(service);
|
|
548
|
+
|
|
549
|
+
// Create entity with UserTag - hook should execute
|
|
550
|
+
const userEntity = Entity.Create();
|
|
551
|
+
userEntity.add(UserTag, { userType: "regular" });
|
|
552
|
+
await userEntity.save();
|
|
553
|
+
|
|
554
|
+
expect(hookExecuted).toBe(true);
|
|
555
|
+
expect(capturedEntityId).toBe(userEntity.id);
|
|
556
|
+
|
|
557
|
+
// Reset for next test
|
|
558
|
+
hookExecuted = false;
|
|
559
|
+
capturedEntityId = "";
|
|
560
|
+
|
|
561
|
+
// Create entity with AdminTag - hook should execute
|
|
562
|
+
const adminEntity = Entity.Create();
|
|
563
|
+
adminEntity.add(AdminTag, { adminLevel: 2 });
|
|
564
|
+
await adminEntity.save();
|
|
565
|
+
|
|
566
|
+
expect(hookExecuted).toBe(true);
|
|
567
|
+
expect(capturedEntityId).toBe(adminEntity.id);
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
test('should register and execute decorated hook with AND logic', async () => {
|
|
571
|
+
let hookExecuted = false;
|
|
572
|
+
let capturedEntityId: string = "";
|
|
573
|
+
|
|
574
|
+
class TestSuperUserService {
|
|
575
|
+
@ComponentTargetHook("entity.created", {
|
|
576
|
+
includeComponents: [UserTag, AdminTag],
|
|
577
|
+
requireAllIncluded: true // AND logic (default)
|
|
578
|
+
})
|
|
579
|
+
async handleSuperUser(event: EntityCreatedEvent) {
|
|
580
|
+
hookExecuted = true;
|
|
581
|
+
capturedEntityId = event.getEntity().id;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const service = new TestSuperUserService();
|
|
586
|
+
registerDecoratedHooks(service);
|
|
587
|
+
|
|
588
|
+
// Create entity with both UserTag and AdminTag - hook should execute
|
|
589
|
+
const superUserEntity = Entity.Create();
|
|
590
|
+
superUserEntity.add(UserTag, { userType: "admin" });
|
|
591
|
+
superUserEntity.add(AdminTag, { adminLevel: 3 });
|
|
592
|
+
await superUserEntity.save();
|
|
593
|
+
|
|
594
|
+
expect(hookExecuted).toBe(true);
|
|
595
|
+
expect(capturedEntityId).toBe(superUserEntity.id);
|
|
596
|
+
|
|
597
|
+
// Reset for next test
|
|
598
|
+
hookExecuted = false;
|
|
599
|
+
capturedEntityId = "";
|
|
600
|
+
|
|
601
|
+
// Create entity with only UserTag - hook should NOT execute
|
|
602
|
+
const userEntity = Entity.Create();
|
|
603
|
+
userEntity.add(UserTag, { userType: "regular" });
|
|
604
|
+
await userEntity.save();
|
|
605
|
+
|
|
606
|
+
expect(hookExecuted).toBe(false);
|
|
607
|
+
expect(capturedEntityId).toBe("");
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
test('should work with entity update events using decorator', async () => {
|
|
611
|
+
let hookExecuted = false;
|
|
612
|
+
let capturedEntityId: string = "";
|
|
613
|
+
|
|
614
|
+
class TestUserUpdateService {
|
|
615
|
+
@ComponentTargetHook("entity.updated", {
|
|
616
|
+
includeComponents: [UserTag]
|
|
617
|
+
})
|
|
618
|
+
async handleUserUpdated(event: EntityUpdatedEvent) {
|
|
619
|
+
hookExecuted = true;
|
|
620
|
+
capturedEntityId = event.getEntity().id;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
const service = new TestUserUpdateService();
|
|
625
|
+
registerDecoratedHooks(service);
|
|
626
|
+
|
|
627
|
+
// Create and save entity first
|
|
628
|
+
const userEntity = Entity.Create();
|
|
629
|
+
userEntity.add(UserTag, { userType: "regular" });
|
|
630
|
+
await userEntity.save();
|
|
631
|
+
|
|
632
|
+
// Reset for update test
|
|
633
|
+
hookExecuted = false;
|
|
634
|
+
capturedEntityId = "";
|
|
635
|
+
|
|
636
|
+
// Update the entity - hook should execute
|
|
637
|
+
await userEntity.set(UserTag, { userType: "premium" });
|
|
638
|
+
await userEntity.save();
|
|
639
|
+
|
|
640
|
+
expect(hookExecuted).toBe(true);
|
|
641
|
+
expect(capturedEntityId).toBe(userEntity.id);
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
test('should support multiple decorated hooks on same service', async () => {
|
|
645
|
+
let userHookExecuted = false;
|
|
646
|
+
let adminHookExecuted = false;
|
|
647
|
+
let userEntityId: string = "";
|
|
648
|
+
let adminEntityId: string = "";
|
|
649
|
+
|
|
650
|
+
class TestMultiHookService {
|
|
651
|
+
@ComponentTargetHook("entity.created", {
|
|
652
|
+
includeComponents: [UserTag]
|
|
653
|
+
})
|
|
654
|
+
async handleUserCreated(event: EntityCreatedEvent) {
|
|
655
|
+
userHookExecuted = true;
|
|
656
|
+
userEntityId = event.getEntity().id;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
@ComponentTargetHook("entity.created", {
|
|
660
|
+
includeComponents: [AdminTag]
|
|
661
|
+
})
|
|
662
|
+
async handleAdminCreated(event: EntityCreatedEvent) {
|
|
663
|
+
adminHookExecuted = true;
|
|
664
|
+
adminEntityId = event.getEntity().id;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
const service = new TestMultiHookService();
|
|
669
|
+
registerDecoratedHooks(service);
|
|
670
|
+
|
|
671
|
+
// Create user entity - only user hook should execute
|
|
672
|
+
const userEntity = Entity.Create();
|
|
673
|
+
userEntity.add(UserTag, { userType: "regular" });
|
|
674
|
+
await userEntity.save();
|
|
675
|
+
|
|
676
|
+
expect(userHookExecuted).toBe(true);
|
|
677
|
+
expect(adminHookExecuted).toBe(false);
|
|
678
|
+
expect(userEntityId).toBe(userEntity.id);
|
|
679
|
+
|
|
680
|
+
// Reset for next test
|
|
681
|
+
userHookExecuted = false;
|
|
682
|
+
adminHookExecuted = false;
|
|
683
|
+
userEntityId = "";
|
|
684
|
+
adminEntityId = "";
|
|
685
|
+
|
|
686
|
+
// Create admin entity - only admin hook should execute
|
|
687
|
+
const adminEntity = Entity.Create();
|
|
688
|
+
adminEntity.add(AdminTag, { adminLevel: 2 });
|
|
689
|
+
await adminEntity.save();
|
|
690
|
+
|
|
691
|
+
expect(userHookExecuted).toBe(false);
|
|
692
|
+
expect(adminHookExecuted).toBe(true);
|
|
693
|
+
expect(adminEntityId).toBe(adminEntity.id);
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
test('should support additional hook options with component targeting', async () => {
|
|
697
|
+
let hookExecuted = false;
|
|
698
|
+
let executionCount = 0;
|
|
699
|
+
|
|
700
|
+
class TestPriorityService {
|
|
701
|
+
@ComponentTargetHook("entity.created", {
|
|
702
|
+
includeComponents: [UserTag]
|
|
703
|
+
}, {
|
|
704
|
+
priority: 10,
|
|
705
|
+
name: "HighPriorityUserHook"
|
|
706
|
+
})
|
|
707
|
+
async handleUserCreated(event: EntityCreatedEvent) {
|
|
708
|
+
hookExecuted = true;
|
|
709
|
+
executionCount++;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
const service = new TestPriorityService();
|
|
714
|
+
registerDecoratedHooks(service);
|
|
715
|
+
|
|
716
|
+
// Create entity with UserTag - hook should execute
|
|
717
|
+
const userEntity = Entity.Create();
|
|
718
|
+
userEntity.add(UserTag, { userType: "premium" });
|
|
719
|
+
await userEntity.save();
|
|
720
|
+
|
|
721
|
+
expect(hookExecuted).toBe(true);
|
|
722
|
+
expect(executionCount).toBe(1);
|
|
723
|
+
});
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
describe('Archetype-Based Component Targeting - Phase 2', () => {
|
|
727
|
+
beforeEach(() => {
|
|
728
|
+
hookManager.clearAllHooks();
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
afterEach(() => {
|
|
732
|
+
hookManager.clearAllHooks();
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
test('should execute hook for entities matching specific archetype', async () => {
|
|
736
|
+
let hookExecuted = false;
|
|
737
|
+
let capturedEntityId: string = "";
|
|
738
|
+
|
|
739
|
+
// Register hook that only executes for UserArchetype entities
|
|
740
|
+
const hookId = hookManager.registerEntityHook("entity.created",
|
|
741
|
+
(event: EntityCreatedEvent) => {
|
|
742
|
+
hookExecuted = true;
|
|
743
|
+
capturedEntityId = event.getEntity().id;
|
|
744
|
+
},
|
|
745
|
+
{
|
|
746
|
+
componentTarget: {
|
|
747
|
+
archetype: UserArchetype
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
);
|
|
751
|
+
|
|
752
|
+
expect(hookId).toBeDefined();
|
|
753
|
+
|
|
754
|
+
// Create entity matching UserArchetype - hook should execute
|
|
755
|
+
const userEntity = UserArchetype.fill({
|
|
756
|
+
userTag: { userType: "premium" },
|
|
757
|
+
nameComponent: { value: "John Doe" },
|
|
758
|
+
emailComponent: { value: "john@example.com" }
|
|
759
|
+
}).createEntity();
|
|
760
|
+
await userEntity.save();
|
|
761
|
+
|
|
762
|
+
expect(hookExecuted).toBe(true);
|
|
763
|
+
expect(capturedEntityId).toBe(userEntity.id);
|
|
764
|
+
|
|
765
|
+
// Reset for next test
|
|
766
|
+
hookExecuted = false;
|
|
767
|
+
capturedEntityId = "";
|
|
768
|
+
|
|
769
|
+
// Create entity with different archetype - hook should NOT execute
|
|
770
|
+
const postEntity = PostArchetype.fill({
|
|
771
|
+
postTag: { category: "news" }
|
|
772
|
+
}).createEntity();
|
|
773
|
+
await postEntity.save();
|
|
774
|
+
|
|
775
|
+
expect(hookExecuted).toBe(false);
|
|
776
|
+
expect(capturedEntityId).toBe("");
|
|
777
|
+
|
|
778
|
+
// Reset for next test
|
|
779
|
+
hookExecuted = false;
|
|
780
|
+
capturedEntityId = "";
|
|
781
|
+
|
|
782
|
+
// Create entity with UserArchetype but extra component - hook should NOT execute
|
|
783
|
+
const extendedUserEntity = UserArchetype.fill({
|
|
784
|
+
userTag: { userType: "regular" },
|
|
785
|
+
nameComponent: { value: "Jane Doe" },
|
|
786
|
+
emailComponent: { value: "jane@example.com" }
|
|
787
|
+
}).createEntity();
|
|
788
|
+
extendedUserEntity.add(AddressComponent, { street: "123 Main St", city: "Anytown" });
|
|
789
|
+
await extendedUserEntity.save();
|
|
790
|
+
|
|
791
|
+
expect(hookExecuted).toBe(false);
|
|
792
|
+
expect(capturedEntityId).toBe("");
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
test('should execute hook for entities matching any of multiple archetypes', async () => {
|
|
796
|
+
let hookExecuted = false;
|
|
797
|
+
let capturedEntityId: string = "";
|
|
798
|
+
|
|
799
|
+
// Register hook that executes for UserArchetype OR AdminArchetype entities
|
|
800
|
+
const hookId = hookManager.registerEntityHook("entity.created",
|
|
801
|
+
(event: EntityCreatedEvent) => {
|
|
802
|
+
hookExecuted = true;
|
|
803
|
+
capturedEntityId = event.getEntity().id;
|
|
804
|
+
},
|
|
805
|
+
{
|
|
806
|
+
componentTarget: {
|
|
807
|
+
archetypes: [UserArchetype, AdminArchetype]
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
);
|
|
811
|
+
|
|
812
|
+
expect(hookId).toBeDefined();
|
|
813
|
+
|
|
814
|
+
// Create entity matching UserArchetype - hook should execute
|
|
815
|
+
const userEntity = UserArchetype.fill({
|
|
816
|
+
userTag: { userType: "regular" },
|
|
817
|
+
nameComponent: { value: "John Doe" },
|
|
818
|
+
emailComponent: { value: "john@example.com" }
|
|
819
|
+
}).createEntity();
|
|
820
|
+
await userEntity.save();
|
|
821
|
+
|
|
822
|
+
expect(hookExecuted).toBe(true);
|
|
823
|
+
expect(capturedEntityId).toBe(userEntity.id);
|
|
824
|
+
|
|
825
|
+
// Reset for next test
|
|
826
|
+
hookExecuted = false;
|
|
827
|
+
capturedEntityId = "";
|
|
828
|
+
|
|
829
|
+
// Create entity matching AdminArchetype - hook should execute
|
|
830
|
+
const adminEntity = AdminArchetype.fill({
|
|
831
|
+
adminTag: { adminLevel: 2 },
|
|
832
|
+
nameComponent: { value: "Admin User" },
|
|
833
|
+
emailComponent: { value: "admin@example.com" }
|
|
834
|
+
}).createEntity();
|
|
835
|
+
await adminEntity.save();
|
|
836
|
+
|
|
837
|
+
expect(hookExecuted).toBe(true);
|
|
838
|
+
expect(capturedEntityId).toBe(adminEntity.id);
|
|
839
|
+
|
|
840
|
+
// Reset for next test
|
|
841
|
+
hookExecuted = false;
|
|
842
|
+
capturedEntityId = "";
|
|
843
|
+
|
|
844
|
+
// Create entity with different archetype - hook should NOT execute
|
|
845
|
+
const postEntity = PostArchetype.fill({
|
|
846
|
+
postTag: { category: "news" }
|
|
847
|
+
}).createEntity();
|
|
848
|
+
await postEntity.save();
|
|
849
|
+
|
|
850
|
+
expect(hookExecuted).toBe(false);
|
|
851
|
+
expect(capturedEntityId).toBe("");
|
|
852
|
+
});
|
|
853
|
+
|
|
854
|
+
test('should combine archetype and component targeting', async () => {
|
|
855
|
+
let hookExecuted = false;
|
|
856
|
+
let capturedEntityId: string = "";
|
|
857
|
+
|
|
858
|
+
// Register hook for UserArchetype entities with additional AdminTag
|
|
859
|
+
const hookId = hookManager.registerEntityHook("entity.created",
|
|
860
|
+
(event: EntityCreatedEvent) => {
|
|
861
|
+
hookExecuted = true;
|
|
862
|
+
capturedEntityId = event.getEntity().id;
|
|
863
|
+
},
|
|
864
|
+
{
|
|
865
|
+
componentTarget: {
|
|
866
|
+
archetype: UserArchetype,
|
|
867
|
+
includeComponents: [AdminTag] // Must also have AdminTag
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
);
|
|
871
|
+
|
|
872
|
+
expect(hookId).toBeDefined();
|
|
873
|
+
|
|
874
|
+
// Create entity matching UserArchetype with AdminTag - hook should execute
|
|
875
|
+
const superUserEntity = UserArchetype.fill({
|
|
876
|
+
userTag: { userType: "admin" },
|
|
877
|
+
nameComponent: { value: "Super User" },
|
|
878
|
+
emailComponent: { value: "super@example.com" }
|
|
879
|
+
}).createEntity();
|
|
880
|
+
superUserEntity.add(AdminTag, { adminLevel: 3 });
|
|
881
|
+
await superUserEntity.save();
|
|
882
|
+
|
|
883
|
+
expect(hookExecuted).toBe(true);
|
|
884
|
+
expect(capturedEntityId).toBe(superUserEntity.id);
|
|
885
|
+
|
|
886
|
+
// Reset for next test
|
|
887
|
+
hookExecuted = false;
|
|
888
|
+
capturedEntityId = "";
|
|
889
|
+
|
|
890
|
+
// Create entity matching UserArchetype without AdminTag - hook should NOT execute
|
|
891
|
+
const userEntity = UserArchetype.fill({
|
|
892
|
+
userTag: { userType: "regular" },
|
|
893
|
+
nameComponent: { value: "Regular User" },
|
|
894
|
+
emailComponent: { value: "regular@example.com" }
|
|
895
|
+
}).createEntity();
|
|
896
|
+
await userEntity.save();
|
|
897
|
+
|
|
898
|
+
expect(hookExecuted).toBe(false);
|
|
899
|
+
expect(capturedEntityId).toBe("");
|
|
900
|
+
});
|
|
901
|
+
|
|
902
|
+
test('should work with archetype targeting using decorator', async () => {
|
|
903
|
+
let hookExecuted = false;
|
|
904
|
+
let capturedEntityId: string = "";
|
|
905
|
+
|
|
906
|
+
class TestArchetypeService {
|
|
907
|
+
@ComponentTargetHook("entity.created", {
|
|
908
|
+
archetype: UserArchetype
|
|
909
|
+
})
|
|
910
|
+
async handleUserArchetype(event: EntityCreatedEvent) {
|
|
911
|
+
hookExecuted = true;
|
|
912
|
+
capturedEntityId = event.getEntity().id;
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
const service = new TestArchetypeService();
|
|
917
|
+
registerDecoratedHooks(service);
|
|
918
|
+
|
|
919
|
+
// Create entity matching UserArchetype - hook should execute
|
|
920
|
+
const userEntity = UserArchetype.fill({
|
|
921
|
+
userTag: { userType: "premium" },
|
|
922
|
+
nameComponent: { value: "John Doe" },
|
|
923
|
+
emailComponent: { value: "john@example.com" }
|
|
924
|
+
}).createEntity();
|
|
925
|
+
await userEntity.save();
|
|
926
|
+
|
|
927
|
+
expect(hookExecuted).toBe(true);
|
|
928
|
+
expect(capturedEntityId).toBe(userEntity.id);
|
|
929
|
+
|
|
930
|
+
// Reset for next test
|
|
931
|
+
hookExecuted = false;
|
|
932
|
+
capturedEntityId = "";
|
|
933
|
+
|
|
934
|
+
// Create entity with different archetype - hook should NOT execute
|
|
935
|
+
const postEntity = PostArchetype.fill({
|
|
936
|
+
postTag: { category: "news" }
|
|
937
|
+
}).createEntity();
|
|
938
|
+
await postEntity.save();
|
|
939
|
+
|
|
940
|
+
expect(hookExecuted).toBe(false);
|
|
941
|
+
expect(capturedEntityId).toBe("");
|
|
942
|
+
});
|
|
943
|
+
|
|
944
|
+
test('should work with multiple archetypes using decorator', async () => {
|
|
945
|
+
let hookExecuted = false;
|
|
946
|
+
let capturedEntityId: string = "";
|
|
947
|
+
|
|
948
|
+
class TestMultiArchetypeService {
|
|
949
|
+
@ComponentTargetHook("entity.created", {
|
|
950
|
+
archetypes: [UserArchetype, AdminArchetype]
|
|
951
|
+
})
|
|
952
|
+
async handleUserOrAdminArchetype(event: EntityCreatedEvent) {
|
|
953
|
+
hookExecuted = true;
|
|
954
|
+
capturedEntityId = event.getEntity().id;
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
const service = new TestMultiArchetypeService();
|
|
959
|
+
registerDecoratedHooks(service);
|
|
960
|
+
|
|
961
|
+
// Create entity matching UserArchetype - hook should execute
|
|
962
|
+
const userEntity = UserArchetype.fill({
|
|
963
|
+
userTag: { userType: "regular" },
|
|
964
|
+
nameComponent: { value: "John Doe" },
|
|
965
|
+
emailComponent: { value: "john@example.com" }
|
|
966
|
+
}).createEntity();
|
|
967
|
+
await userEntity.save();
|
|
968
|
+
|
|
969
|
+
expect(hookExecuted).toBe(true);
|
|
970
|
+
expect(capturedEntityId).toBe(userEntity.id);
|
|
971
|
+
|
|
972
|
+
// Reset for next test
|
|
973
|
+
hookExecuted = false;
|
|
974
|
+
capturedEntityId = "";
|
|
975
|
+
|
|
976
|
+
// Create entity matching AdminArchetype - hook should execute
|
|
977
|
+
const adminEntity = AdminArchetype.fill({
|
|
978
|
+
adminTag: { adminLevel: 2 },
|
|
979
|
+
nameComponent: { value: "Admin User" },
|
|
980
|
+
emailComponent: { value: "admin@example.com" }
|
|
981
|
+
}).createEntity();
|
|
982
|
+
await adminEntity.save();
|
|
983
|
+
|
|
984
|
+
expect(hookExecuted).toBe(true);
|
|
985
|
+
expect(capturedEntityId).toBe(adminEntity.id);
|
|
986
|
+
});
|
|
987
|
+
});
|
|
988
|
+
|
|
989
|
+
describe('Batch Processing Optimizations - Phase 2', () => {
|
|
990
|
+
beforeEach(() => {
|
|
991
|
+
hookManager.clearAllHooks();
|
|
992
|
+
});
|
|
993
|
+
|
|
994
|
+
afterEach(() => {
|
|
995
|
+
hookManager.clearAllHooks();
|
|
996
|
+
});
|
|
997
|
+
|
|
998
|
+
test('should efficiently process multiple events in batch with component targeting', async () => {
|
|
999
|
+
let executionCount = 0;
|
|
1000
|
+
let executedEntityIds: string[] = [];
|
|
1001
|
+
|
|
1002
|
+
// Register hook that only executes for entities with UserTag
|
|
1003
|
+
const hookId = hookManager.registerEntityHook("entity.created",
|
|
1004
|
+
(event: EntityCreatedEvent) => {
|
|
1005
|
+
executionCount++;
|
|
1006
|
+
executedEntityIds.push(event.getEntity().id);
|
|
1007
|
+
},
|
|
1008
|
+
{
|
|
1009
|
+
componentTarget: {
|
|
1010
|
+
includeComponents: [UserTag]
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
);
|
|
1014
|
+
|
|
1015
|
+
expect(hookId).toBeDefined();
|
|
1016
|
+
|
|
1017
|
+
// Create multiple entities in batch
|
|
1018
|
+
const events: EntityCreatedEvent[] = [];
|
|
1019
|
+
const userEntities: Entity[] = [];
|
|
1020
|
+
const postEntities: Entity[] = [];
|
|
1021
|
+
|
|
1022
|
+
// Create 3 user entities and 2 post entities
|
|
1023
|
+
for (let i = 0; i < 3; i++) {
|
|
1024
|
+
const userEntity = Entity.Create();
|
|
1025
|
+
userEntity.add(UserTag, { userType: `user${i}` });
|
|
1026
|
+
userEntities.push(userEntity);
|
|
1027
|
+
events.push(new EntityCreatedEvent(userEntity));
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
for (let i = 0; i < 2; i++) {
|
|
1031
|
+
const postEntity = Entity.Create();
|
|
1032
|
+
postEntity.add(PostTag, { category: `category${i}` });
|
|
1033
|
+
postEntities.push(postEntity);
|
|
1034
|
+
events.push(new EntityCreatedEvent(postEntity));
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
// Execute hooks in batch
|
|
1038
|
+
await hookManager.executeHooksBatch(events);
|
|
1039
|
+
|
|
1040
|
+
// Hook should have executed only for user entities
|
|
1041
|
+
expect(executionCount).toBe(3);
|
|
1042
|
+
expect(executedEntityIds).toHaveLength(3);
|
|
1043
|
+
|
|
1044
|
+
// Verify the correct entities were processed
|
|
1045
|
+
const userEntityIds = userEntities.map(e => e.id);
|
|
1046
|
+
expect(executedEntityIds.sort()).toEqual(userEntityIds.sort());
|
|
1047
|
+
});
|
|
1048
|
+
|
|
1049
|
+
test('should optimize batch processing by pre-filtering hooks', async () => {
|
|
1050
|
+
let userHookExecuted = false;
|
|
1051
|
+
let adminHookExecuted = false;
|
|
1052
|
+
let postHookExecuted = false;
|
|
1053
|
+
|
|
1054
|
+
// Register multiple hooks with different component targeting
|
|
1055
|
+
const userHookId = hookManager.registerEntityHook("entity.created",
|
|
1056
|
+
(event: EntityCreatedEvent) => {
|
|
1057
|
+
userHookExecuted = true;
|
|
1058
|
+
},
|
|
1059
|
+
{
|
|
1060
|
+
componentTarget: {
|
|
1061
|
+
includeComponents: [UserTag]
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
);
|
|
1065
|
+
|
|
1066
|
+
const adminHookId = hookManager.registerEntityHook("entity.created",
|
|
1067
|
+
(event: EntityCreatedEvent) => {
|
|
1068
|
+
adminHookExecuted = true;
|
|
1069
|
+
},
|
|
1070
|
+
{
|
|
1071
|
+
componentTarget: {
|
|
1072
|
+
includeComponents: [AdminTag]
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
);
|
|
1076
|
+
|
|
1077
|
+
const postHookId = hookManager.registerEntityHook("entity.created",
|
|
1078
|
+
(event: EntityCreatedEvent) => {
|
|
1079
|
+
postHookExecuted = true;
|
|
1080
|
+
},
|
|
1081
|
+
{
|
|
1082
|
+
componentTarget: {
|
|
1083
|
+
includeComponents: [PostTag]
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
);
|
|
1087
|
+
|
|
1088
|
+
// Create batch of events with only UserTag entities
|
|
1089
|
+
const events: EntityCreatedEvent[] = [];
|
|
1090
|
+
for (let i = 0; i < 3; i++) {
|
|
1091
|
+
const userEntity = Entity.Create();
|
|
1092
|
+
userEntity.add(UserTag, { userType: `user${i}` });
|
|
1093
|
+
events.push(new EntityCreatedEvent(userEntity));
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
// Execute hooks in batch - only user hook should execute
|
|
1097
|
+
await hookManager.executeHooksBatch(events);
|
|
1098
|
+
|
|
1099
|
+
expect(userHookExecuted).toBe(true);
|
|
1100
|
+
expect(adminHookExecuted).toBe(false);
|
|
1101
|
+
expect(postHookExecuted).toBe(false);
|
|
1102
|
+
});
|
|
1103
|
+
|
|
1104
|
+
test('should handle mixed sync and async hooks efficiently in batch', async () => {
|
|
1105
|
+
let syncExecutionCount = 0;
|
|
1106
|
+
let asyncExecutionCount = 0;
|
|
1107
|
+
let syncEntityIds: string[] = [];
|
|
1108
|
+
let asyncEntityIds: string[] = [];
|
|
1109
|
+
|
|
1110
|
+
// Register sync hook
|
|
1111
|
+
const syncHookId = hookManager.registerEntityHook("entity.created",
|
|
1112
|
+
(event: EntityCreatedEvent) => {
|
|
1113
|
+
syncExecutionCount++;
|
|
1114
|
+
syncEntityIds.push(event.getEntity().id);
|
|
1115
|
+
},
|
|
1116
|
+
{
|
|
1117
|
+
componentTarget: {
|
|
1118
|
+
includeComponents: [UserTag]
|
|
1119
|
+
},
|
|
1120
|
+
async: false
|
|
1121
|
+
}
|
|
1122
|
+
);
|
|
1123
|
+
|
|
1124
|
+
// Register async hook
|
|
1125
|
+
const asyncHookId = hookManager.registerEntityHook("entity.created",
|
|
1126
|
+
async (event: EntityCreatedEvent) => {
|
|
1127
|
+
await new Promise(resolve => setTimeout(resolve, 1)); // Simulate async work
|
|
1128
|
+
asyncExecutionCount++;
|
|
1129
|
+
asyncEntityIds.push(event.getEntity().id);
|
|
1130
|
+
},
|
|
1131
|
+
{
|
|
1132
|
+
componentTarget: {
|
|
1133
|
+
includeComponents: [UserTag]
|
|
1134
|
+
},
|
|
1135
|
+
async: true
|
|
1136
|
+
}
|
|
1137
|
+
);
|
|
1138
|
+
|
|
1139
|
+
// Create batch of user entities
|
|
1140
|
+
const events: EntityCreatedEvent[] = [];
|
|
1141
|
+
const userEntities: Entity[] = [];
|
|
1142
|
+
|
|
1143
|
+
for (let i = 0; i < 3; i++) {
|
|
1144
|
+
const userEntity = Entity.Create();
|
|
1145
|
+
userEntity.add(UserTag, { userType: `user${i}` });
|
|
1146
|
+
userEntities.push(userEntity);
|
|
1147
|
+
events.push(new EntityCreatedEvent(userEntity));
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
// Execute hooks in batch
|
|
1151
|
+
await hookManager.executeHooksBatch(events);
|
|
1152
|
+
|
|
1153
|
+
// Both hooks should have executed for all user entities
|
|
1154
|
+
expect(syncExecutionCount).toBe(3);
|
|
1155
|
+
expect(asyncExecutionCount).toBe(3);
|
|
1156
|
+
|
|
1157
|
+
const userEntityIds = userEntities.map(e => e.id).sort();
|
|
1158
|
+
expect(syncEntityIds.sort()).toEqual(userEntityIds);
|
|
1159
|
+
expect(asyncEntityIds.sort()).toEqual(userEntityIds);
|
|
1160
|
+
});
|
|
1161
|
+
|
|
1162
|
+
test('should maintain hook execution order in batch processing', async () => {
|
|
1163
|
+
let executionOrder: string[] = [];
|
|
1164
|
+
|
|
1165
|
+
// Register hooks with different priorities
|
|
1166
|
+
const highPriorityHookId = hookManager.registerEntityHook("entity.created",
|
|
1167
|
+
(event: EntityCreatedEvent) => {
|
|
1168
|
+
executionOrder.push(`high-${event.getEntity().id}`);
|
|
1169
|
+
},
|
|
1170
|
+
{
|
|
1171
|
+
componentTarget: {
|
|
1172
|
+
includeComponents: [UserTag]
|
|
1173
|
+
},
|
|
1174
|
+
priority: 10
|
|
1175
|
+
}
|
|
1176
|
+
);
|
|
1177
|
+
|
|
1178
|
+
const lowPriorityHookId = hookManager.registerEntityHook("entity.created",
|
|
1179
|
+
(event: EntityCreatedEvent) => {
|
|
1180
|
+
executionOrder.push(`low-${event.getEntity().id}`);
|
|
1181
|
+
},
|
|
1182
|
+
{
|
|
1183
|
+
componentTarget: {
|
|
1184
|
+
includeComponents: [UserTag]
|
|
1185
|
+
},
|
|
1186
|
+
priority: 1
|
|
1187
|
+
}
|
|
1188
|
+
);
|
|
1189
|
+
|
|
1190
|
+
// Create batch of user entities
|
|
1191
|
+
const events: EntityCreatedEvent[] = [];
|
|
1192
|
+
const userEntities: Entity[] = [];
|
|
1193
|
+
|
|
1194
|
+
for (let i = 0; i < 2; i++) {
|
|
1195
|
+
const userEntity = Entity.Create();
|
|
1196
|
+
userEntity.add(UserTag, { userType: `user${i}` });
|
|
1197
|
+
userEntities.push(userEntity);
|
|
1198
|
+
events.push(new EntityCreatedEvent(userEntity));
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
// Execute hooks in batch
|
|
1202
|
+
await hookManager.executeHooksBatch(events);
|
|
1203
|
+
|
|
1204
|
+
// Verify execution order (high priority first)
|
|
1205
|
+
expect(executionOrder).toHaveLength(4);
|
|
1206
|
+
expect(executionOrder[0]).toMatch(/^high-/);
|
|
1207
|
+
expect(executionOrder[1]).toMatch(/^high-/);
|
|
1208
|
+
expect(executionOrder[2]).toMatch(/^low-/);
|
|
1209
|
+
expect(executionOrder[3]).toMatch(/^low-/);
|
|
1210
|
+
});
|
|
1211
|
+
|
|
1212
|
+
test('should handle archetype-based targeting efficiently in batch', async () => {
|
|
1213
|
+
let executionCount = 0;
|
|
1214
|
+
let executedEntityIds: string[] = [];
|
|
1215
|
+
|
|
1216
|
+
// Register hook for UserArchetype entities
|
|
1217
|
+
const hookId = hookManager.registerEntityHook("entity.created",
|
|
1218
|
+
(event: EntityCreatedEvent) => {
|
|
1219
|
+
executionCount++;
|
|
1220
|
+
executedEntityIds.push(event.getEntity().id);
|
|
1221
|
+
},
|
|
1222
|
+
{
|
|
1223
|
+
componentTarget: {
|
|
1224
|
+
archetype: UserArchetype
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
);
|
|
1228
|
+
|
|
1229
|
+
// Create batch of mixed entities
|
|
1230
|
+
const events: EntityCreatedEvent[] = [];
|
|
1231
|
+
const userEntities: Entity[] = [];
|
|
1232
|
+
const adminEntities: Entity[] = [];
|
|
1233
|
+
const postEntities: Entity[] = [];
|
|
1234
|
+
|
|
1235
|
+
// Create 2 user archetype entities
|
|
1236
|
+
for (let i = 0; i < 2; i++) {
|
|
1237
|
+
const userEntity = UserArchetype.fill({
|
|
1238
|
+
userTag: { userType: `user${i}` },
|
|
1239
|
+
nameComponent: { value: `User ${i}` },
|
|
1240
|
+
emailComponent: { value: `user${i}@example.com` }
|
|
1241
|
+
}).createEntity();
|
|
1242
|
+
userEntities.push(userEntity);
|
|
1243
|
+
events.push(new EntityCreatedEvent(userEntity));
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
// Create 1 admin archetype entity
|
|
1247
|
+
const adminEntity = AdminArchetype.fill({
|
|
1248
|
+
adminTag: { adminLevel: 2 },
|
|
1249
|
+
nameComponent: { value: "Admin User" },
|
|
1250
|
+
emailComponent: { value: "admin@example.com" }
|
|
1251
|
+
}).createEntity();
|
|
1252
|
+
adminEntities.push(adminEntity);
|
|
1253
|
+
events.push(new EntityCreatedEvent(adminEntity));
|
|
1254
|
+
|
|
1255
|
+
// Create 1 post entity
|
|
1256
|
+
const postEntity = PostArchetype.fill({
|
|
1257
|
+
postTag: { category: "news" }
|
|
1258
|
+
}).createEntity();
|
|
1259
|
+
postEntities.push(postEntity);
|
|
1260
|
+
events.push(new EntityCreatedEvent(postEntity));
|
|
1261
|
+
|
|
1262
|
+
// Execute hooks in batch
|
|
1263
|
+
await hookManager.executeHooksBatch(events);
|
|
1264
|
+
|
|
1265
|
+
// Hook should have executed only for user archetype entities
|
|
1266
|
+
expect(executionCount).toBe(2);
|
|
1267
|
+
expect(executedEntityIds).toHaveLength(2);
|
|
1268
|
+
|
|
1269
|
+
const userEntityIds = userEntities.map(e => e.id);
|
|
1270
|
+
expect(executedEntityIds.sort()).toEqual(userEntityIds.sort());
|
|
1271
|
+
});
|
|
1272
|
+
|
|
1273
|
+
test('should handle timeout and error scenarios in batch processing', async () => {
|
|
1274
|
+
let successCount = 0;
|
|
1275
|
+
let errorCount = 0;
|
|
1276
|
+
|
|
1277
|
+
// Register hook that succeeds
|
|
1278
|
+
const successHookId = hookManager.registerEntityHook("entity.created",
|
|
1279
|
+
(event: EntityCreatedEvent) => {
|
|
1280
|
+
successCount++;
|
|
1281
|
+
},
|
|
1282
|
+
{
|
|
1283
|
+
componentTarget: {
|
|
1284
|
+
includeComponents: [UserTag]
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
);
|
|
1288
|
+
|
|
1289
|
+
// Register hook that throws error
|
|
1290
|
+
const errorHookId = hookManager.registerEntityHook("entity.created",
|
|
1291
|
+
(event: EntityCreatedEvent) => {
|
|
1292
|
+
errorCount++;
|
|
1293
|
+
throw new Error("Test error");
|
|
1294
|
+
},
|
|
1295
|
+
{
|
|
1296
|
+
componentTarget: {
|
|
1297
|
+
includeComponents: [UserTag]
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
);
|
|
1301
|
+
|
|
1302
|
+
// Create batch of user entities
|
|
1303
|
+
const events: EntityCreatedEvent[] = [];
|
|
1304
|
+
for (let i = 0; i < 2; i++) {
|
|
1305
|
+
const userEntity = Entity.Create();
|
|
1306
|
+
userEntity.add(UserTag, { userType: `user${i}` });
|
|
1307
|
+
events.push(new EntityCreatedEvent(userEntity));
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
// Execute hooks in batch - should handle errors gracefully
|
|
1311
|
+
await hookManager.executeHooksBatch(events);
|
|
1312
|
+
|
|
1313
|
+
// Success hook should have executed for both entities
|
|
1314
|
+
expect(successCount).toBe(2);
|
|
1315
|
+
// Error hook should have executed but thrown errors
|
|
1316
|
+
expect(errorCount).toBe(2);
|
|
1317
|
+
});
|
|
1318
|
+
|
|
1319
|
+
test('should optimize performance with large batches and component targeting', async () => {
|
|
1320
|
+
let executionCount = 0;
|
|
1321
|
+
|
|
1322
|
+
// Register hook with component targeting
|
|
1323
|
+
const hookId = hookManager.registerEntityHook("entity.created",
|
|
1324
|
+
(event: EntityCreatedEvent) => {
|
|
1325
|
+
executionCount++;
|
|
1326
|
+
},
|
|
1327
|
+
{
|
|
1328
|
+
componentTarget: {
|
|
1329
|
+
includeComponents: [UserTag]
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
);
|
|
1333
|
+
|
|
1334
|
+
// Create large batch of mixed entities (100 total: 50 users, 50 posts)
|
|
1335
|
+
const events: EntityCreatedEvent[] = [];
|
|
1336
|
+
const userEntities: Entity[] = [];
|
|
1337
|
+
|
|
1338
|
+
for (let i = 0; i < 50; i++) {
|
|
1339
|
+
const userEntity = Entity.Create();
|
|
1340
|
+
userEntity.add(UserTag, { userType: `user${i}` });
|
|
1341
|
+
userEntities.push(userEntity);
|
|
1342
|
+
events.push(new EntityCreatedEvent(userEntity));
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
for (let i = 0; i < 50; i++) {
|
|
1346
|
+
const postEntity = Entity.Create();
|
|
1347
|
+
postEntity.add(PostTag, { category: `category${i}` });
|
|
1348
|
+
events.push(new EntityCreatedEvent(postEntity));
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
// Execute hooks in batch
|
|
1352
|
+
const startTime = performance.now();
|
|
1353
|
+
await hookManager.executeHooksBatch(events);
|
|
1354
|
+
const endTime = performance.now();
|
|
1355
|
+
|
|
1356
|
+
// Hook should have executed only for user entities
|
|
1357
|
+
expect(executionCount).toBe(50);
|
|
1358
|
+
|
|
1359
|
+
// Verify reasonable performance (should complete in reasonable time)
|
|
1360
|
+
const executionTime = endTime - startTime;
|
|
1361
|
+
expect(executionTime).toBeLessThan(1000); // Should complete in less than 1 second
|
|
1362
|
+
});
|
|
1363
|
+
|
|
1364
|
+
test('should work with decorator-based hooks in batch processing', async () => {
|
|
1365
|
+
let executionCount = 0;
|
|
1366
|
+
let executedEntityIds: string[] = [];
|
|
1367
|
+
|
|
1368
|
+
class TestBatchService {
|
|
1369
|
+
@ComponentTargetHook("entity.created", {
|
|
1370
|
+
includeComponents: [UserTag]
|
|
1371
|
+
})
|
|
1372
|
+
async handleUserCreated(event: EntityCreatedEvent) {
|
|
1373
|
+
executionCount++;
|
|
1374
|
+
executedEntityIds.push(event.getEntity().id);
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
const service = new TestBatchService();
|
|
1379
|
+
registerDecoratedHooks(service);
|
|
1380
|
+
|
|
1381
|
+
// Create batch of mixed entities
|
|
1382
|
+
const events: EntityCreatedEvent[] = [];
|
|
1383
|
+
const userEntities: Entity[] = [];
|
|
1384
|
+
|
|
1385
|
+
for (let i = 0; i < 3; i++) {
|
|
1386
|
+
const userEntity = Entity.Create();
|
|
1387
|
+
userEntity.add(UserTag, { userType: `user${i}` });
|
|
1388
|
+
userEntities.push(userEntity);
|
|
1389
|
+
events.push(new EntityCreatedEvent(userEntity));
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
// Add non-matching entities
|
|
1393
|
+
for (let i = 0; i < 2; i++) {
|
|
1394
|
+
const postEntity = Entity.Create();
|
|
1395
|
+
postEntity.add(PostTag, { category: `category${i}` });
|
|
1396
|
+
events.push(new EntityCreatedEvent(postEntity));
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
// Execute hooks in batch
|
|
1400
|
+
await hookManager.executeHooksBatch(events);
|
|
1401
|
+
|
|
1402
|
+
// Decorator hook should have executed only for user entities
|
|
1403
|
+
expect(executionCount).toBe(3);
|
|
1404
|
+
expect(executedEntityIds).toHaveLength(3);
|
|
1405
|
+
|
|
1406
|
+
const userEntityIds = userEntities.map(e => e.id);
|
|
1407
|
+
expect(executedEntityIds.sort()).toEqual(userEntityIds.sort());
|
|
1408
|
+
});
|
|
1409
|
+
});
|