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,666 @@
|
|
|
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, ComponentAddedEvent, ComponentUpdatedEvent, ComponentRemovedEvent } from "../core/events/EntityLifecycleEvents";
|
|
5
|
+
import { BaseComponent, CompData, Component } from "../core/Components";
|
|
6
|
+
import App from "../core/App";
|
|
7
|
+
import { EntityHook, ComponentHook, LifecycleHook, registerDecoratedHooks } from "../core/decorators/EntityHooks";
|
|
8
|
+
|
|
9
|
+
let app: App;
|
|
10
|
+
|
|
11
|
+
beforeAll(async () => {
|
|
12
|
+
app = new App();
|
|
13
|
+
await app.waitForAppReady();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
@Component
|
|
17
|
+
class TestComponent extends BaseComponent {
|
|
18
|
+
@CompData()
|
|
19
|
+
value: string = "";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@Component
|
|
23
|
+
class AnotherTestComponent extends BaseComponent {
|
|
24
|
+
@CompData()
|
|
25
|
+
numberValue: number = 0;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe('Entity Lifecycle Hooks - Phase 1', () => {
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
hookManager.clearAllHooks();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
afterEach(() => {
|
|
34
|
+
hookManager.clearAllHooks();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('should register and execute entity created hook', async () => {
|
|
38
|
+
let hookExecuted = false;
|
|
39
|
+
let capturedEvent: EntityCreatedEvent | null = null;
|
|
40
|
+
|
|
41
|
+
const hookId = hookManager.registerEntityHook("entity.created", (event: EntityCreatedEvent) => {
|
|
42
|
+
hookExecuted = true;
|
|
43
|
+
capturedEvent = event;
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
expect(hookId).toBeDefined();
|
|
47
|
+
expect(typeof hookId).toBe('string');
|
|
48
|
+
|
|
49
|
+
// Create and save a new entity
|
|
50
|
+
const entity = Entity.Create();
|
|
51
|
+
await entity.save();
|
|
52
|
+
|
|
53
|
+
// Verify hook was executed
|
|
54
|
+
expect(hookExecuted).toBe(true);
|
|
55
|
+
expect(capturedEvent).toBeInstanceOf(EntityCreatedEvent);
|
|
56
|
+
expect((capturedEvent as unknown as EntityCreatedEvent)?.getEntity()).toBe(entity);
|
|
57
|
+
expect((capturedEvent as unknown as EntityCreatedEvent)?.isNew).toBe(true);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('should register and execute entity updated hook', async () => {
|
|
61
|
+
let hookExecuted = false;
|
|
62
|
+
let capturedEvent: EntityUpdatedEvent | null = null;
|
|
63
|
+
|
|
64
|
+
const hookId = hookManager.registerEntityHook("entity.updated", (event: EntityUpdatedEvent) => {
|
|
65
|
+
hookExecuted = true;
|
|
66
|
+
capturedEvent = event;
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Create and save a new entity first
|
|
70
|
+
const entity = Entity.Create().add(TestComponent, { value: "initial" });
|
|
71
|
+
await entity.save();
|
|
72
|
+
|
|
73
|
+
// Reset for update test
|
|
74
|
+
hookExecuted = false;
|
|
75
|
+
capturedEvent = null;
|
|
76
|
+
|
|
77
|
+
// Update the entity
|
|
78
|
+
await entity.set(TestComponent, { value: "updated" });
|
|
79
|
+
await entity.save();
|
|
80
|
+
|
|
81
|
+
// Verify hook was executed
|
|
82
|
+
expect(hookExecuted).toBe(true);
|
|
83
|
+
expect(capturedEvent).toBeInstanceOf(EntityUpdatedEvent);
|
|
84
|
+
expect((capturedEvent as unknown as EntityUpdatedEvent)?.getEntity()).toBe(entity);
|
|
85
|
+
expect((capturedEvent as unknown as EntityUpdatedEvent)?.isNew).toBe(false);
|
|
86
|
+
expect((capturedEvent as unknown as EntityUpdatedEvent)?.getChangedComponents()).toHaveLength(1);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('should register and execute entity deleted hook', async () => {
|
|
90
|
+
let hookExecuted = false;
|
|
91
|
+
let capturedEvent: EntityDeletedEvent | null = null;
|
|
92
|
+
|
|
93
|
+
const hookId = hookManager.registerEntityHook("entity.deleted", (event: EntityDeletedEvent) => {
|
|
94
|
+
hookExecuted = true;
|
|
95
|
+
capturedEvent = event;
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// Create and save a new entity first
|
|
99
|
+
const entity = Entity.Create().add(TestComponent, { value: "test" });
|
|
100
|
+
await entity.save();
|
|
101
|
+
|
|
102
|
+
// Delete the entity
|
|
103
|
+
await entity.delete();
|
|
104
|
+
|
|
105
|
+
// Verify hook was executed
|
|
106
|
+
expect(hookExecuted).toBe(true);
|
|
107
|
+
expect(capturedEvent).toBeInstanceOf(EntityDeletedEvent);
|
|
108
|
+
expect((capturedEvent as unknown as EntityDeletedEvent)?.getEntity()).toBe(entity);
|
|
109
|
+
expect((capturedEvent as unknown as EntityDeletedEvent)?.isSoftDelete).toBe(true);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('should execute multiple hooks for the same event', async () => {
|
|
113
|
+
let hook1Executed = false;
|
|
114
|
+
let hook2Executed = false;
|
|
115
|
+
|
|
116
|
+
hookManager.registerEntityHook("entity.created", () => {
|
|
117
|
+
hook1Executed = true;
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
hookManager.registerEntityHook("entity.created", () => {
|
|
121
|
+
hook2Executed = true;
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// Create and save a new entity
|
|
125
|
+
const entity = Entity.Create();
|
|
126
|
+
await entity.save();
|
|
127
|
+
|
|
128
|
+
// Verify both hooks were executed
|
|
129
|
+
expect(hook1Executed).toBe(true);
|
|
130
|
+
expect(hook2Executed).toBe(true);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('should execute hooks with priority ordering', async () => {
|
|
134
|
+
const executionOrder: number[] = [];
|
|
135
|
+
|
|
136
|
+
hookManager.registerEntityHook("entity.created", () => {
|
|
137
|
+
executionOrder.push(1);
|
|
138
|
+
}, { priority: 1 });
|
|
139
|
+
|
|
140
|
+
hookManager.registerEntityHook("entity.created", () => {
|
|
141
|
+
executionOrder.push(2);
|
|
142
|
+
}, { priority: 2 });
|
|
143
|
+
|
|
144
|
+
hookManager.registerEntityHook("entity.created", () => {
|
|
145
|
+
executionOrder.push(3);
|
|
146
|
+
}, { priority: 0 });
|
|
147
|
+
|
|
148
|
+
// Create and save a new entity
|
|
149
|
+
const entity = Entity.Create();
|
|
150
|
+
await entity.save();
|
|
151
|
+
|
|
152
|
+
// Verify hooks executed in priority order (highest first)
|
|
153
|
+
expect(executionOrder).toEqual([2, 1, 3]);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('should handle hook execution errors gracefully', async () => {
|
|
157
|
+
let goodHookExecuted = false;
|
|
158
|
+
|
|
159
|
+
hookManager.registerEntityHook("entity.created", () => {
|
|
160
|
+
throw new Error("Hook failed");
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
hookManager.registerEntityHook("entity.created", () => {
|
|
164
|
+
goodHookExecuted = true;
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// Create and save a new entity
|
|
168
|
+
const entity = Entity.Create();
|
|
169
|
+
await entity.save();
|
|
170
|
+
|
|
171
|
+
// Verify the good hook still executed despite the error
|
|
172
|
+
expect(goodHookExecuted).toBe(true);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test('should remove hooks by ID', () => {
|
|
176
|
+
const hookId = hookManager.registerEntityHook("entity.created", () => {
|
|
177
|
+
// This should not execute
|
|
178
|
+
expect(true).toBe(false);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const removed = hookManager.removeHook(hookId);
|
|
182
|
+
expect(removed).toBe(true);
|
|
183
|
+
|
|
184
|
+
// Verify hook count decreased
|
|
185
|
+
expect(hookManager.getHookCount("entity.created")).toBe(0);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test('should return false when removing non-existent hook', () => {
|
|
189
|
+
const removed = hookManager.removeHook("non-existent-hook-id");
|
|
190
|
+
expect(removed).toBe(false);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test('should get correct hook counts', () => {
|
|
194
|
+
expect(hookManager.getHookCount()).toBe(0);
|
|
195
|
+
|
|
196
|
+
hookManager.registerEntityHook("entity.created", () => {});
|
|
197
|
+
hookManager.registerEntityHook("entity.updated", () => {});
|
|
198
|
+
hookManager.registerEntityHook("entity.created", () => {});
|
|
199
|
+
|
|
200
|
+
expect(hookManager.getHookCount()).toBe(3);
|
|
201
|
+
expect(hookManager.getHookCount("entity.created")).toBe(2);
|
|
202
|
+
expect(hookManager.getHookCount("entity.updated")).toBe(1);
|
|
203
|
+
expect(hookManager.getHookCount("entity.deleted")).toBe(0);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test('should clear all hooks', () => {
|
|
207
|
+
hookManager.registerEntityHook("entity.created", () => {});
|
|
208
|
+
hookManager.registerEntityHook("entity.updated", () => {});
|
|
209
|
+
|
|
210
|
+
expect(hookManager.getHookCount()).toBe(2);
|
|
211
|
+
|
|
212
|
+
hookManager.clearAllHooks();
|
|
213
|
+
|
|
214
|
+
expect(hookManager.getHookCount()).toBe(0);
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
describe('Component Lifecycle Hooks - Phase 2', () => {
|
|
219
|
+
beforeEach(() => {
|
|
220
|
+
hookManager.clearAllHooks();
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
afterEach(() => {
|
|
224
|
+
hookManager.clearAllHooks();
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test('should register and execute component added hook', async () => {
|
|
228
|
+
let hookExecuted = false;
|
|
229
|
+
let capturedEvent: ComponentAddedEvent | null = null;
|
|
230
|
+
|
|
231
|
+
const hookId = hookManager.registerComponentHook("component.added", (event: ComponentAddedEvent) => {
|
|
232
|
+
hookExecuted = true;
|
|
233
|
+
capturedEvent = event;
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
expect(hookId).toBeDefined();
|
|
237
|
+
expect(typeof hookId).toBe('string');
|
|
238
|
+
|
|
239
|
+
// Create entity and add component
|
|
240
|
+
const entity = Entity.Create();
|
|
241
|
+
const testComponent = new TestComponent();
|
|
242
|
+
testComponent.value = "test component";
|
|
243
|
+
entity.add(TestComponent, { value: "test component" });
|
|
244
|
+
|
|
245
|
+
// Verify hook was executed
|
|
246
|
+
expect(hookExecuted).toBe(true);
|
|
247
|
+
expect(capturedEvent).toBeInstanceOf(ComponentAddedEvent);
|
|
248
|
+
expect((capturedEvent as unknown as ComponentAddedEvent)?.getEntity()).toBe(entity);
|
|
249
|
+
expect((capturedEvent as unknown as ComponentAddedEvent)?.getComponentType()).toBe(testComponent.getTypeID());
|
|
250
|
+
expect(((capturedEvent as unknown as ComponentAddedEvent)?.getComponent().data() as any).value).toBe("test component");
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test('should register and execute component updated hook', async () => {
|
|
254
|
+
let hookExecuted = false;
|
|
255
|
+
let capturedEvent: ComponentUpdatedEvent | null = null;
|
|
256
|
+
|
|
257
|
+
const hookId = hookManager.registerComponentHook("component.updated", (event: ComponentUpdatedEvent) => {
|
|
258
|
+
hookExecuted = true;
|
|
259
|
+
capturedEvent = event;
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
// Create entity and add component first
|
|
263
|
+
const entity = Entity.Create();
|
|
264
|
+
entity.add(TestComponent, { value: "initial value" });
|
|
265
|
+
|
|
266
|
+
// Reset for update test
|
|
267
|
+
hookExecuted = false;
|
|
268
|
+
capturedEvent = null;
|
|
269
|
+
|
|
270
|
+
// Update the component
|
|
271
|
+
await entity.set(TestComponent, { value: "updated value" });
|
|
272
|
+
|
|
273
|
+
// Verify hook was executed
|
|
274
|
+
expect(hookExecuted).toBe(true);
|
|
275
|
+
expect(capturedEvent).toBeInstanceOf(ComponentUpdatedEvent);
|
|
276
|
+
expect((capturedEvent as unknown as ComponentUpdatedEvent)?.getEntity()).toBe(entity);
|
|
277
|
+
expect((capturedEvent as unknown as ComponentUpdatedEvent)?.getComponentType()).toBe((new TestComponent()).getTypeID());
|
|
278
|
+
expect((capturedEvent as unknown as ComponentUpdatedEvent)?.getOldData()?.value).toBe("initial value");
|
|
279
|
+
expect((capturedEvent as unknown as ComponentUpdatedEvent)?.getNewData()?.value).toBe("updated value");
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
test('should register and execute component removed hook', async () => {
|
|
283
|
+
let hookExecuted = false;
|
|
284
|
+
let capturedEvent: ComponentRemovedEvent | null = null;
|
|
285
|
+
|
|
286
|
+
const hookId = hookManager.registerComponentHook("component.removed", (event: ComponentRemovedEvent) => {
|
|
287
|
+
hookExecuted = true;
|
|
288
|
+
capturedEvent = event;
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
// Create entity and add component first
|
|
292
|
+
const entity = Entity.Create();
|
|
293
|
+
entity.add(TestComponent, { value: "test component" });
|
|
294
|
+
|
|
295
|
+
// Remove the component
|
|
296
|
+
const removed = entity.remove(TestComponent);
|
|
297
|
+
|
|
298
|
+
// Verify removal was successful
|
|
299
|
+
expect(removed).toBe(true);
|
|
300
|
+
|
|
301
|
+
// Verify hook was executed
|
|
302
|
+
expect(hookExecuted).toBe(true);
|
|
303
|
+
expect(capturedEvent).toBeInstanceOf(ComponentRemovedEvent);
|
|
304
|
+
expect((capturedEvent as unknown as ComponentRemovedEvent)?.getEntity()).toBe(entity);
|
|
305
|
+
expect((capturedEvent as unknown as ComponentRemovedEvent)?.getComponentType()).toBe((new TestComponent()).getTypeID());
|
|
306
|
+
expect(((capturedEvent as unknown as ComponentRemovedEvent)?.getComponent().data() as any).value).toBe("test component");
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test('should execute multiple component hooks for the same event', async () => {
|
|
310
|
+
let hook1Executed = false;
|
|
311
|
+
let hook2Executed = false;
|
|
312
|
+
|
|
313
|
+
hookManager.registerComponentHook("component.added", () => {
|
|
314
|
+
hook1Executed = true;
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
hookManager.registerComponentHook("component.added", () => {
|
|
318
|
+
hook2Executed = true;
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
// Create entity and add component
|
|
322
|
+
const entity = Entity.Create();
|
|
323
|
+
entity.add(TestComponent, { value: "test" });
|
|
324
|
+
|
|
325
|
+
// Verify both hooks were executed
|
|
326
|
+
expect(hook1Executed).toBe(true);
|
|
327
|
+
expect(hook2Executed).toBe(true);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
test('should execute component hooks with priority ordering', async () => {
|
|
331
|
+
const executionOrder: number[] = [];
|
|
332
|
+
|
|
333
|
+
hookManager.registerComponentHook("component.added", () => {
|
|
334
|
+
executionOrder.push(1);
|
|
335
|
+
}, { priority: 1 });
|
|
336
|
+
|
|
337
|
+
hookManager.registerComponentHook("component.added", () => {
|
|
338
|
+
executionOrder.push(2);
|
|
339
|
+
}, { priority: 2 });
|
|
340
|
+
|
|
341
|
+
hookManager.registerComponentHook("component.added", () => {
|
|
342
|
+
executionOrder.push(3);
|
|
343
|
+
}, { priority: 0 });
|
|
344
|
+
|
|
345
|
+
// Create entity and add component
|
|
346
|
+
const entity = Entity.Create();
|
|
347
|
+
entity.add(TestComponent, { value: "test" });
|
|
348
|
+
|
|
349
|
+
// Verify hooks executed in priority order (highest first)
|
|
350
|
+
expect(executionOrder).toEqual([2, 1, 3]);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
test('should handle component hook execution errors gracefully', async () => {
|
|
354
|
+
let goodHookExecuted = false;
|
|
355
|
+
|
|
356
|
+
hookManager.registerComponentHook("component.added", () => {
|
|
357
|
+
throw new Error("Component hook failed");
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
hookManager.registerComponentHook("component.added", () => {
|
|
361
|
+
goodHookExecuted = true;
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
// Create entity and add component
|
|
365
|
+
const entity = Entity.Create();
|
|
366
|
+
entity.add(TestComponent, { value: "test" });
|
|
367
|
+
|
|
368
|
+
// Verify the good hook still executed despite the error
|
|
369
|
+
expect(goodHookExecuted).toBe(true);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
test('should return false when removing non-existent component', () => {
|
|
373
|
+
const entity = Entity.Create();
|
|
374
|
+
|
|
375
|
+
// Try to remove a component that doesn't exist
|
|
376
|
+
const removed = entity.remove(TestComponent);
|
|
377
|
+
|
|
378
|
+
expect(removed).toBe(false);
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
test('should fire component added event when set() adds new component', async () => {
|
|
382
|
+
let hookExecuted = false;
|
|
383
|
+
let capturedEvent: ComponentAddedEvent | null = null;
|
|
384
|
+
|
|
385
|
+
const hookId = hookManager.registerComponentHook("component.added", (event: ComponentAddedEvent) => {
|
|
386
|
+
hookExecuted = true;
|
|
387
|
+
capturedEvent = event;
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
// Create entity and use set() to add new component
|
|
391
|
+
const entity = Entity.Create();
|
|
392
|
+
await entity.set(TestComponent, { value: "new component" });
|
|
393
|
+
|
|
394
|
+
// Verify hook was executed (set() should fire added event for new components)
|
|
395
|
+
expect(hookExecuted).toBe(true);
|
|
396
|
+
expect(capturedEvent).toBeInstanceOf(ComponentAddedEvent);
|
|
397
|
+
expect(((capturedEvent as unknown as ComponentAddedEvent)?.getComponent().data() as any).value).toBe("new component");
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
test('should fire component updated event when set() updates existing component', async () => {
|
|
401
|
+
let hookExecuted = false;
|
|
402
|
+
let capturedEvent: ComponentUpdatedEvent | null = null;
|
|
403
|
+
|
|
404
|
+
const hookId = hookManager.registerComponentHook("component.updated", (event: ComponentUpdatedEvent) => {
|
|
405
|
+
hookExecuted = true;
|
|
406
|
+
capturedEvent = event;
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
// Create entity and add component first
|
|
410
|
+
const entity = Entity.Create();
|
|
411
|
+
entity.add(TestComponent, { value: "initial" });
|
|
412
|
+
|
|
413
|
+
// Reset for update test
|
|
414
|
+
hookExecuted = false;
|
|
415
|
+
capturedEvent = null;
|
|
416
|
+
|
|
417
|
+
// Use set() to update existing component
|
|
418
|
+
await entity.set(TestComponent, { value: "updated" });
|
|
419
|
+
|
|
420
|
+
// Verify hook was executed
|
|
421
|
+
expect(hookExecuted).toBe(true);
|
|
422
|
+
expect(capturedEvent).toBeInstanceOf(ComponentUpdatedEvent);
|
|
423
|
+
expect((capturedEvent as unknown as ComponentUpdatedEvent)?.getOldData()?.value).toBe("initial");
|
|
424
|
+
expect((capturedEvent as unknown as ComponentUpdatedEvent)?.getNewData()?.value).toBe("updated");
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
test('should get correct hook counts for component events', () => {
|
|
428
|
+
expect(hookManager.getHookCount()).toBe(0);
|
|
429
|
+
|
|
430
|
+
hookManager.registerComponentHook("component.added", () => {});
|
|
431
|
+
hookManager.registerComponentHook("component.updated", () => {});
|
|
432
|
+
hookManager.registerComponentHook("component.added", () => {});
|
|
433
|
+
|
|
434
|
+
expect(hookManager.getHookCount()).toBe(3);
|
|
435
|
+
expect(hookManager.getHookCount("component.added")).toBe(2);
|
|
436
|
+
expect(hookManager.getHookCount("component.updated")).toBe(1);
|
|
437
|
+
expect(hookManager.getHookCount("component.removed")).toBe(0);
|
|
438
|
+
});
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
describe('Advanced Features - Phase 3', () => {
|
|
442
|
+
beforeEach(() => {
|
|
443
|
+
hookManager.clearAllHooks();
|
|
444
|
+
hookManager.resetMetrics();
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
afterEach(() => {
|
|
448
|
+
hookManager.clearAllHooks();
|
|
449
|
+
hookManager.resetMetrics();
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
test('should execute async hooks properly', async () => {
|
|
453
|
+
let hookExecuted = false;
|
|
454
|
+
let executionOrder: string[] = [];
|
|
455
|
+
|
|
456
|
+
const hookId = hookManager.registerEntityHook("entity.created", async (event: EntityCreatedEvent) => {
|
|
457
|
+
executionOrder.push('start');
|
|
458
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
459
|
+
hookExecuted = true;
|
|
460
|
+
executionOrder.push('end');
|
|
461
|
+
}, { async: true });
|
|
462
|
+
|
|
463
|
+
// Create entity and save
|
|
464
|
+
const entity = Entity.Create();
|
|
465
|
+
await entity.save();
|
|
466
|
+
|
|
467
|
+
// Verify hook was executed asynchronously
|
|
468
|
+
expect(hookExecuted).toBe(true);
|
|
469
|
+
expect(executionOrder).toEqual(['start', 'end']);
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
test('should handle hook timeouts', async () => {
|
|
473
|
+
let hookExecuted = false;
|
|
474
|
+
let timeoutErrorThrown = false;
|
|
475
|
+
|
|
476
|
+
const hookId = hookManager.registerEntityHook("entity.created", async (event: EntityCreatedEvent) => {
|
|
477
|
+
await new Promise(resolve => setTimeout(resolve, 100)); // Longer than timeout
|
|
478
|
+
hookExecuted = true;
|
|
479
|
+
}, { async: true, timeout: 50 });
|
|
480
|
+
|
|
481
|
+
// Create entity and save
|
|
482
|
+
const entity = Entity.Create();
|
|
483
|
+
await entity.save();
|
|
484
|
+
|
|
485
|
+
// Wait a bit for timeout to occur and be logged
|
|
486
|
+
await new Promise(resolve => setTimeout(resolve, 60));
|
|
487
|
+
|
|
488
|
+
// Hook should have executed (timeout doesn't prevent execution, just logs error)
|
|
489
|
+
expect(hookExecuted).toBe(true);
|
|
490
|
+
// The timeout error should have been logged (we can't easily test the exact log output in this test)
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
test('should filter hooks based on conditions', async () => {
|
|
494
|
+
let executedHooks: string[] = [];
|
|
495
|
+
|
|
496
|
+
// Hook that only executes for new entities
|
|
497
|
+
hookManager.registerEntityHook("entity.created", (event: EntityCreatedEvent) => {
|
|
498
|
+
executedHooks.push('new-only');
|
|
499
|
+
}, {
|
|
500
|
+
filter: (event) => event instanceof EntityCreatedEvent && event.isNew
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
// Hook that executes for all created events
|
|
504
|
+
hookManager.registerEntityHook("entity.created", (event: EntityCreatedEvent) => {
|
|
505
|
+
executedHooks.push('all');
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
// Create and save entity (should be new)
|
|
509
|
+
const entity = Entity.Create();
|
|
510
|
+
await entity.save();
|
|
511
|
+
|
|
512
|
+
// Verify both hooks executed (filter should pass for new entity)
|
|
513
|
+
expect(executedHooks).toContain('new-only');
|
|
514
|
+
expect(executedHooks).toContain('all');
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
test('should collect performance metrics', async () => {
|
|
518
|
+
const hookId = hookManager.registerEntityHook("entity.created", (event: EntityCreatedEvent) => {
|
|
519
|
+
// Small delay to simulate work
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
// Create entity and save
|
|
523
|
+
const entity = Entity.Create();
|
|
524
|
+
await entity.save();
|
|
525
|
+
|
|
526
|
+
// Check metrics
|
|
527
|
+
const metrics = hookManager.getMetrics("entity.created");
|
|
528
|
+
expect(metrics.totalExecutions).toBe(1);
|
|
529
|
+
expect(metrics.averageExecutionTime).toBeGreaterThan(0);
|
|
530
|
+
expect(metrics.errorCount).toBe(0);
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
test('should reset metrics correctly', async () => {
|
|
534
|
+
const hookId = hookManager.registerEntityHook("entity.created", (event: EntityCreatedEvent) => {
|
|
535
|
+
// Do nothing
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
// Execute hook
|
|
539
|
+
const entity = Entity.Create();
|
|
540
|
+
await entity.save();
|
|
541
|
+
|
|
542
|
+
// Verify metrics exist
|
|
543
|
+
expect(hookManager.getMetrics("entity.created").totalExecutions).toBe(1);
|
|
544
|
+
|
|
545
|
+
// Reset metrics
|
|
546
|
+
hookManager.resetMetrics("entity.created");
|
|
547
|
+
|
|
548
|
+
// Verify metrics are reset
|
|
549
|
+
expect(hookManager.getMetrics("entity.created").totalExecutions).toBe(0);
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
test('should execute hooks in batch', async () => {
|
|
553
|
+
let executionCount = 0;
|
|
554
|
+
|
|
555
|
+
const hookId = hookManager.registerEntityHook("entity.created", (event: EntityCreatedEvent) => {
|
|
556
|
+
executionCount++;
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
// Create multiple events
|
|
560
|
+
const events = [
|
|
561
|
+
new EntityCreatedEvent(Entity.Create()),
|
|
562
|
+
new EntityCreatedEvent(Entity.Create()),
|
|
563
|
+
new EntityCreatedEvent(Entity.Create())
|
|
564
|
+
];
|
|
565
|
+
|
|
566
|
+
// Execute in batch
|
|
567
|
+
await hookManager.executeHooksBatch(events);
|
|
568
|
+
|
|
569
|
+
// Verify all hooks were executed
|
|
570
|
+
expect(executionCount).toBe(3);
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
test('should work with decorator-based hooks', async () => {
|
|
574
|
+
let hookExecuted = false;
|
|
575
|
+
|
|
576
|
+
class TestService {
|
|
577
|
+
@EntityHook("entity.created")
|
|
578
|
+
async handleEntityCreated(event: EntityCreatedEvent) {
|
|
579
|
+
hookExecuted = true;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const service = new TestService();
|
|
584
|
+
registerDecoratedHooks(service);
|
|
585
|
+
|
|
586
|
+
// Create entity and save
|
|
587
|
+
const entity = Entity.Create();
|
|
588
|
+
await entity.save();
|
|
589
|
+
|
|
590
|
+
// Verify decorated hook was executed
|
|
591
|
+
expect(hookExecuted).toBe(true);
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
test('should work with component decorator hooks', async () => {
|
|
595
|
+
let hookExecuted = false;
|
|
596
|
+
|
|
597
|
+
class TestService {
|
|
598
|
+
@ComponentHook("component.added")
|
|
599
|
+
async handleComponentAdded(event: ComponentAddedEvent) {
|
|
600
|
+
hookExecuted = true;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
const service = new TestService();
|
|
605
|
+
registerDecoratedHooks(service);
|
|
606
|
+
|
|
607
|
+
// Create entity and add component
|
|
608
|
+
const entity = Entity.Create();
|
|
609
|
+
entity.add(TestComponent, { value: "test" });
|
|
610
|
+
|
|
611
|
+
// Verify decorated hook was executed
|
|
612
|
+
expect(hookExecuted).toBe(true);
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
test('should work with lifecycle decorator hooks', async () => {
|
|
616
|
+
let hookExecuted = false;
|
|
617
|
+
|
|
618
|
+
class TestService {
|
|
619
|
+
@LifecycleHook()
|
|
620
|
+
async handleAnyLifecycleEvent(event: any) {
|
|
621
|
+
if (event.getEventType() === "entity.created") {
|
|
622
|
+
hookExecuted = true;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
const service = new TestService();
|
|
628
|
+
registerDecoratedHooks(service);
|
|
629
|
+
|
|
630
|
+
// Create entity and save
|
|
631
|
+
const entity = Entity.Create();
|
|
632
|
+
await entity.save();
|
|
633
|
+
|
|
634
|
+
// Verify decorated hook was executed
|
|
635
|
+
expect(hookExecuted).toBe(true);
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
test('should work with multiple decorator hooks on same service', async () => {
|
|
639
|
+
let entityHookExecuted = false;
|
|
640
|
+
let componentHookExecuted = false;
|
|
641
|
+
|
|
642
|
+
class TestService {
|
|
643
|
+
@EntityHook("entity.created")
|
|
644
|
+
async handleEntityCreated(event: EntityCreatedEvent) {
|
|
645
|
+
entityHookExecuted = true;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
@ComponentHook("component.added")
|
|
649
|
+
async handleComponentAdded(event: ComponentAddedEvent) {
|
|
650
|
+
componentHookExecuted = true;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
const service = new TestService();
|
|
655
|
+
registerDecoratedHooks(service);
|
|
656
|
+
|
|
657
|
+
// Create entity and add component
|
|
658
|
+
const entity = Entity.Create();
|
|
659
|
+
entity.add(TestComponent, { value: "test" });
|
|
660
|
+
await entity.save();
|
|
661
|
+
|
|
662
|
+
// Verify both decorated hooks were executed
|
|
663
|
+
expect(entityHookExecuted).toBe(true);
|
|
664
|
+
expect(componentHookExecuted).toBe(true);
|
|
665
|
+
});
|
|
666
|
+
});
|