bunsane 0.1.4 → 0.2.0

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