bunsane 0.3.1 → 0.4.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 (224) hide show
  1. package/CHANGELOG.md +445 -318
  2. package/config/cache.config.ts +35 -1
  3. package/core/App.ts +24 -1064
  4. package/core/ArcheType.ts +78 -2110
  5. package/core/BatchLoader.ts +56 -32
  6. package/core/Entity.ts +85 -1043
  7. package/core/EntityHookManager.ts +52 -754
  8. package/core/Logger.ts +10 -0
  9. package/core/RequestContext.ts +64 -6
  10. package/core/RequestLoaders.ts +187 -36
  11. package/core/SchedulerManager.ts +28 -600
  12. package/core/app/bootstrap.ts +133 -0
  13. package/core/app/cors.ts +85 -0
  14. package/core/app/graphqlSetup.ts +56 -0
  15. package/core/app/healthEndpoints.ts +31 -0
  16. package/core/app/metricsCollector.ts +27 -0
  17. package/core/app/preparedStatementWarmup.ts +15 -0
  18. package/core/app/processHandlers.ts +43 -0
  19. package/core/app/requestRouter.ts +310 -0
  20. package/core/app/restRegistry.ts +80 -0
  21. package/core/app/shutdown.ts +97 -0
  22. package/core/app/studioRouter.ts +83 -0
  23. package/core/archetype/customTypes.ts +100 -0
  24. package/core/archetype/decorators.ts +171 -0
  25. package/core/archetype/fieldResolvers.ts +666 -0
  26. package/core/archetype/helpers.ts +29 -0
  27. package/core/archetype/relationLoader.ts +161 -0
  28. package/core/archetype/schemaBuilder.ts +141 -0
  29. package/core/archetype/weaver.ts +218 -0
  30. package/core/archetype/zodSchemaBuilder.ts +527 -0
  31. package/core/cache/CacheManager.ts +173 -267
  32. package/core/cache/CompressionUtils.ts +34 -3
  33. package/core/cache/MemoryCache.ts +40 -37
  34. package/core/cache/RedisCache.ts +4 -4
  35. package/core/cache/health.ts +30 -0
  36. package/core/cache/invalidation.ts +96 -0
  37. package/core/cache/strategies/writeInvalidate.ts +111 -0
  38. package/core/cache/strategies/writeThrough.ts +233 -0
  39. package/core/components/BaseComponent.ts +16 -8
  40. package/core/components/ComponentRegistry.ts +28 -0
  41. package/core/decorators/IndexedField.ts +1 -1
  42. package/core/entity/cacheStrategies.ts +97 -0
  43. package/core/entity/componentAccess.ts +364 -0
  44. package/core/entity/finders.ts +202 -0
  45. package/core/entity/pendingOps.ts +72 -0
  46. package/core/entity/saveEntity.ts +377 -0
  47. package/core/hooks/dispatcher.ts +439 -0
  48. package/core/hooks/guards.ts +155 -0
  49. package/core/hooks/registry.ts +247 -0
  50. package/core/metadata/definitions/Component.ts +1 -1
  51. package/core/metadata/index.ts +15 -4
  52. package/core/middleware/AccessLog.ts +8 -1
  53. package/core/middleware/RateLimit.ts +102 -105
  54. package/core/middleware/RequestId.ts +2 -9
  55. package/core/middleware/SecurityHeaders.ts +2 -11
  56. package/core/middleware/headers.ts +28 -0
  57. package/core/remote/OutboxWorker.ts +213 -183
  58. package/core/remote/RemoteManager.ts +401 -400
  59. package/core/remote/types.ts +153 -151
  60. package/core/requestScope.ts +34 -0
  61. package/core/scheduler/cronEvaluator.ts +174 -0
  62. package/core/scheduler/lifecycleHooks.ts +21 -0
  63. package/core/scheduler/lockCoordinator.ts +27 -0
  64. package/core/scheduler/metrics.ts +14 -0
  65. package/core/scheduler/taskRunner.ts +420 -0
  66. package/database/DatabaseHelper.ts +128 -101
  67. package/database/IndexingStrategy.ts +72 -2
  68. package/database/PreparedStatementCache.ts +20 -5
  69. package/database/cancellable.ts +35 -0
  70. package/database/index.ts +15 -3
  71. package/database/instrumentedDb.ts +141 -0
  72. package/endpoints/archetypes.ts +2 -8
  73. package/endpoints/tables.ts +6 -1
  74. package/gql/index.ts +1 -1
  75. package/gql/visitors/ResolverGeneratorVisitor.ts +25 -4
  76. package/package.json +22 -1
  77. package/query/CTENode.ts +5 -3
  78. package/query/ComponentInclusionNode.ts +240 -13
  79. package/query/OrNode.ts +6 -5
  80. package/query/Query.ts +203 -59
  81. package/query/QueryContext.ts +6 -0
  82. package/query/QueryDAG.ts +7 -2
  83. package/query/membershipSource.ts +66 -0
  84. package/storage/LocalStorageProvider.ts +8 -3
  85. package/studio/dist/assets/index-BMZ67Npg.js +254 -0
  86. package/studio/dist/assets/index-BpbuYz9g.css +1 -0
  87. package/studio/{index.html → dist/index.html} +3 -2
  88. package/swagger/generator.ts +11 -1
  89. package/upload/UploadManager.ts +8 -6
  90. package/utils/uuid.ts +40 -10
  91. package/.claude/settings.local.json +0 -47
  92. package/.prettierrc +0 -4
  93. package/.serena/memories/architectural-decision-no-dependency-injection.md +0 -76
  94. package/.serena/memories/architecture.md +0 -154
  95. package/.serena/memories/cache-interface-refactoring-2026-01-24.md +0 -165
  96. package/.serena/memories/code_style_and_conventions.md +0 -76
  97. package/.serena/memories/project_overview.md +0 -43
  98. package/.serena/memories/schema-dsl-plan.md +0 -107
  99. package/.serena/memories/suggested_commands.md +0 -80
  100. package/.serena/memories/typescript-compilation-status.md +0 -54
  101. package/.serena/project.yml +0 -114
  102. package/BunSane.jpg +0 -0
  103. package/CLAUDE.md +0 -198
  104. package/TODO.md +0 -2
  105. package/bun.lock +0 -302
  106. package/bunfig.toml +0 -10
  107. package/docs/SCALABILITY_PLAN.md +0 -175
  108. package/studio/bun.lock +0 -482
  109. package/studio/package.json +0 -39
  110. package/studio/postcss.config.js +0 -6
  111. package/studio/src/components/DataTable.tsx +0 -211
  112. package/studio/src/components/Layout.tsx +0 -13
  113. package/studio/src/components/PageContainer.tsx +0 -9
  114. package/studio/src/components/PageHeader.tsx +0 -13
  115. package/studio/src/components/SearchBar.tsx +0 -57
  116. package/studio/src/components/Sidebar.tsx +0 -294
  117. package/studio/src/components/ui/button.tsx +0 -56
  118. package/studio/src/components/ui/checkbox.tsx +0 -26
  119. package/studio/src/components/ui/input.tsx +0 -25
  120. package/studio/src/hooks/useDataTable.ts +0 -131
  121. package/studio/src/index.css +0 -36
  122. package/studio/src/lib/api.ts +0 -186
  123. package/studio/src/lib/utils.ts +0 -13
  124. package/studio/src/main.tsx +0 -17
  125. package/studio/src/pages/ArcheType.tsx +0 -239
  126. package/studio/src/pages/Components.tsx +0 -124
  127. package/studio/src/pages/EntityInspector.tsx +0 -302
  128. package/studio/src/pages/QueryRunner.tsx +0 -246
  129. package/studio/src/pages/Table.tsx +0 -94
  130. package/studio/src/pages/Welcome.tsx +0 -241
  131. package/studio/src/routes.tsx +0 -45
  132. package/studio/src/store/archeTypeSettings.ts +0 -30
  133. package/studio/src/store/studio.ts +0 -65
  134. package/studio/src/utils/columnHelpers.tsx +0 -114
  135. package/studio/studio-instructions.md +0 -81
  136. package/studio/tailwind.config.js +0 -77
  137. package/studio/utils.ts +0 -54
  138. package/studio/vite.config.js +0 -19
  139. package/tests/benchmark/BENCHMARK_DATABASES_PLAN.md +0 -338
  140. package/tests/benchmark/bunfig.toml +0 -9
  141. package/tests/benchmark/fixtures/EcommerceComponents.ts +0 -283
  142. package/tests/benchmark/fixtures/EcommerceDataGenerators.ts +0 -301
  143. package/tests/benchmark/fixtures/RelationTracker.ts +0 -159
  144. package/tests/benchmark/fixtures/index.ts +0 -6
  145. package/tests/benchmark/index.ts +0 -22
  146. package/tests/benchmark/noop-preload.ts +0 -3
  147. package/tests/benchmark/query-lateral-benchmark.test.ts +0 -372
  148. package/tests/benchmark/runners/BenchmarkLoader.ts +0 -132
  149. package/tests/benchmark/runners/index.ts +0 -4
  150. package/tests/benchmark/scenarios/query-benchmarks.test.ts +0 -465
  151. package/tests/benchmark/scripts/generate-db.ts +0 -344
  152. package/tests/benchmark/scripts/run-benchmarks.ts +0 -97
  153. package/tests/e2e/http.test.ts +0 -130
  154. package/tests/fixtures/archetypes/TestUserArchetype.ts +0 -21
  155. package/tests/fixtures/components/TestOrder.ts +0 -23
  156. package/tests/fixtures/components/TestProduct.ts +0 -23
  157. package/tests/fixtures/components/TestUser.ts +0 -20
  158. package/tests/fixtures/components/index.ts +0 -6
  159. package/tests/graphql/SchemaGeneration.test.ts +0 -90
  160. package/tests/graphql/builders/ResolverBuilder.test.ts +0 -223
  161. package/tests/graphql/builders/TypeDefBuilder.test.ts +0 -153
  162. package/tests/helpers/MockRedisClient.ts +0 -113
  163. package/tests/helpers/MockRedisStreamServer.ts +0 -448
  164. package/tests/integration/archetype/ArcheType.persistence.test.ts +0 -241
  165. package/tests/integration/cache/CacheInvalidation.test.ts +0 -259
  166. package/tests/integration/entity/Entity.persistence.test.ts +0 -333
  167. package/tests/integration/entity/Entity.saveTimeout.test.ts +0 -110
  168. package/tests/integration/query/Query.complexAnalysis.test.ts +0 -557
  169. package/tests/integration/query/Query.edgeCases.test.ts +0 -595
  170. package/tests/integration/query/Query.exec.test.ts +0 -576
  171. package/tests/integration/query/Query.explainAnalyze.test.ts +0 -233
  172. package/tests/integration/query/Query.jsonbArray.test.ts +0 -214
  173. package/tests/integration/remote/dlq.test.ts +0 -175
  174. package/tests/integration/remote/event-dispatch.test.ts +0 -114
  175. package/tests/integration/remote/outbox.test.ts +0 -130
  176. package/tests/integration/remote/rpc.test.ts +0 -177
  177. package/tests/pglite-setup.ts +0 -62
  178. package/tests/setup.ts +0 -164
  179. package/tests/stress/BenchmarkRunner.ts +0 -203
  180. package/tests/stress/DataSeeder.ts +0 -190
  181. package/tests/stress/StressTestReporter.ts +0 -229
  182. package/tests/stress/cursor-perf-test.ts +0 -171
  183. package/tests/stress/fixtures/RealisticComponents.ts +0 -235
  184. package/tests/stress/fixtures/StressTestComponents.ts +0 -58
  185. package/tests/stress/index.ts +0 -7
  186. package/tests/stress/scenarios/query-benchmarks.test.ts +0 -285
  187. package/tests/stress/scenarios/realistic-scenarios.test.ts +0 -1081
  188. package/tests/stress/scenarios/timeout-investigation.test.ts +0 -522
  189. package/tests/unit/BatchLoader.test.ts +0 -196
  190. package/tests/unit/archetype/ArcheType.test.ts +0 -107
  191. package/tests/unit/cache/CacheManager.test.ts +0 -367
  192. package/tests/unit/cache/MemoryCache.test.ts +0 -260
  193. package/tests/unit/cache/RedisCache.test.ts +0 -411
  194. package/tests/unit/entity/Entity.components.test.ts +0 -317
  195. package/tests/unit/entity/Entity.drainSideEffects.test.ts +0 -51
  196. package/tests/unit/entity/Entity.reload.test.ts +0 -63
  197. package/tests/unit/entity/Entity.requireComponents.test.ts +0 -72
  198. package/tests/unit/entity/Entity.test.ts +0 -345
  199. package/tests/unit/gql/depthLimit.test.ts +0 -203
  200. package/tests/unit/gql/operationMiddleware.test.ts +0 -293
  201. package/tests/unit/health/Health.test.ts +0 -129
  202. package/tests/unit/middleware/AccessLog.test.ts +0 -37
  203. package/tests/unit/middleware/Middleware.test.ts +0 -98
  204. package/tests/unit/middleware/RequestId.test.ts +0 -54
  205. package/tests/unit/middleware/SecurityHeaders.test.ts +0 -66
  206. package/tests/unit/query/FilterBuilder.test.ts +0 -111
  207. package/tests/unit/query/JsonbArrayBuilder.test.ts +0 -178
  208. package/tests/unit/query/Query.emptyString.test.ts +0 -69
  209. package/tests/unit/query/Query.test.ts +0 -310
  210. package/tests/unit/remote/CircuitBreaker.test.ts +0 -159
  211. package/tests/unit/remote/RemoteError.test.ts +0 -55
  212. package/tests/unit/remote/decorators.test.ts +0 -195
  213. package/tests/unit/remote/metrics.test.ts +0 -115
  214. package/tests/unit/remote/mockRedisStreamServer.test.ts +0 -104
  215. package/tests/unit/scheduler/DistributedLock.test.ts +0 -274
  216. package/tests/unit/scheduler/SchedulerManager.timeBased.test.ts +0 -95
  217. package/tests/unit/schema/schema-integration.test.ts +0 -426
  218. package/tests/unit/schema/schema.test.ts +0 -580
  219. package/tests/unit/storage/S3StorageProvider.test.ts +0 -567
  220. package/tests/unit/upload/RestUpload.test.ts +0 -267
  221. package/tests/unit/validateEnv.test.ts +0 -82
  222. package/tests/utils/entity-tracker.ts +0 -57
  223. package/tests/utils/index.ts +0 -13
  224. package/tests/utils/test-context.ts +0 -149
@@ -1,317 +0,0 @@
1
- /**
2
- * Unit tests for Entity component management
3
- * Tests component add, remove, and data handling
4
- */
5
- import { describe, test, expect, beforeAll } from 'bun:test';
6
- import { Entity } from '../../../core/Entity';
7
- import { TestUser, TestProduct, TestOrder } from '../../fixtures/components';
8
- import { ensureComponentsRegistered } from '../../utils';
9
-
10
- describe('Entity Component Management', () => {
11
- beforeAll(async () => {
12
- await ensureComponentsRegistered(TestUser, TestProduct, TestOrder);
13
- });
14
-
15
- describe('component data handling', () => {
16
- test('component data() returns correct properties', () => {
17
- const entity = new Entity();
18
- entity.add(TestUser, { name: 'John', email: 'john@test.com', age: 30, bio: 'A bio' });
19
-
20
- const component = entity.getInMemory(TestUser);
21
- const data = component?.data();
22
-
23
- expect(data?.name).toBe('John');
24
- expect(data?.email).toBe('john@test.com');
25
- expect(data?.age).toBe(30);
26
- expect(data?.bio).toBe('A bio');
27
- });
28
-
29
- test('component handles nullable fields', () => {
30
- const entity = new Entity();
31
- entity.add(TestUser, { name: 'John', email: 'john@test.com', age: 30 });
32
-
33
- const component = entity.getInMemory(TestUser);
34
- const data = component?.data();
35
-
36
- expect(data?.bio).toBeUndefined();
37
- });
38
-
39
- test('component handles boolean fields', () => {
40
- const entity = new Entity();
41
- entity.add(TestProduct, {
42
- sku: 'SKU123',
43
- name: 'Test Product',
44
- price: 99.99,
45
- inStock: true
46
- });
47
-
48
- const component = entity.getInMemory(TestProduct);
49
- expect(component?.inStock).toBe(true);
50
- });
51
-
52
- test('component handles false boolean correctly', () => {
53
- const entity = new Entity();
54
- entity.add(TestProduct, {
55
- sku: 'SKU123',
56
- name: 'Test Product',
57
- price: 99.99,
58
- inStock: false
59
- });
60
-
61
- const component = entity.getInMemory(TestProduct);
62
- expect(component?.inStock).toBe(false);
63
- });
64
-
65
- test('component handles number fields', () => {
66
- const entity = new Entity();
67
- entity.add(TestProduct, {
68
- sku: 'SKU123',
69
- name: 'Test Product',
70
- price: 123.45,
71
- inStock: true
72
- });
73
-
74
- const component = entity.getInMemory(TestProduct);
75
- expect(component?.price).toBe(123.45);
76
- });
77
-
78
- test('component handles zero value correctly', () => {
79
- const entity = new Entity();
80
- entity.add(TestUser, { name: 'John', email: 'john@test.com', age: 0 });
81
-
82
- const component = entity.getInMemory(TestUser);
83
- expect(component?.age).toBe(0);
84
- });
85
-
86
- test('component handles Date fields', () => {
87
- const date = new Date('2024-01-15T10:30:00Z');
88
- const entity = new Entity();
89
- entity.add(TestOrder, {
90
- orderNumber: 'ORD-001',
91
- total: 150.00,
92
- status: 'pending',
93
- createdAt: date
94
- });
95
-
96
- const component = entity.getInMemory(TestOrder);
97
- expect(component?.createdAt).toEqual(date);
98
- });
99
- });
100
-
101
- describe('multiple components', () => {
102
- test('handles multiple components of different types', () => {
103
- const entity = new Entity();
104
- entity.add(TestUser, { name: 'John', email: 'john@test.com', age: 30 });
105
- entity.add(TestProduct, { sku: 'SKU1', name: 'Product 1', price: 50, inStock: true });
106
- entity.add(TestOrder, {
107
- orderNumber: 'ORD-001',
108
- total: 150,
109
- status: 'completed',
110
- createdAt: new Date()
111
- });
112
-
113
- expect(entity.componentList().length).toBe(3);
114
- expect(entity.hasInMemory(TestUser)).toBe(true);
115
- expect(entity.hasInMemory(TestProduct)).toBe(true);
116
- expect(entity.hasInMemory(TestOrder)).toBe(true);
117
- });
118
-
119
- test('each component maintains its own data', () => {
120
- const entity = new Entity();
121
- entity.add(TestUser, { name: 'User Name', email: 'user@test.com', age: 25 });
122
- entity.add(TestProduct, { sku: 'PROD1', name: 'Product Name', price: 100, inStock: true });
123
-
124
- const user = entity.getInMemory(TestUser);
125
- const product = entity.getInMemory(TestProduct);
126
-
127
- expect(user?.name).toBe('User Name');
128
- expect(product?.name).toBe('Product Name');
129
- });
130
-
131
- test('removing one component does not affect others', () => {
132
- const entity = new Entity();
133
- entity.add(TestUser, { name: 'John', email: 'john@test.com', age: 30 });
134
- entity.add(TestProduct, { sku: 'SKU1', name: 'Product', price: 50, inStock: true });
135
-
136
- entity.remove(TestUser);
137
-
138
- expect(entity.hasInMemory(TestUser)).toBe(false);
139
- expect(entity.hasInMemory(TestProduct)).toBe(true);
140
- expect(entity.getInMemory(TestProduct)?.name).toBe('Product');
141
- });
142
- });
143
-
144
- describe('component replacement', () => {
145
- test('adding same component type replaces existing', () => {
146
- const entity = new Entity();
147
- entity.add(TestUser, { name: 'Original', email: 'original@test.com', age: 30 });
148
- entity.add(TestUser, { name: 'Replaced', email: 'replaced@test.com', age: 25 });
149
-
150
- const components = entity.componentList();
151
- expect(components.length).toBe(1);
152
-
153
- const user = entity.getInMemory(TestUser);
154
- expect(user?.name).toBe('Replaced');
155
- expect(user?.email).toBe('replaced@test.com');
156
- });
157
- });
158
-
159
- describe('component serialization', () => {
160
- test('serializableData returns proper format', () => {
161
- const entity = new Entity();
162
- entity.add(TestUser, { name: 'John', email: 'john@test.com', age: 30 });
163
-
164
- const component = entity.getInMemory(TestUser);
165
- const data = component?.serializableData();
166
-
167
- expect(data).toEqual({
168
- name: 'John',
169
- email: 'john@test.com',
170
- age: 30,
171
- bio: undefined
172
- });
173
- });
174
-
175
- test('serializableData handles Date as ISO string', () => {
176
- const date = new Date('2024-01-15T10:30:00.000Z');
177
- const entity = new Entity();
178
- entity.add(TestOrder, {
179
- orderNumber: 'ORD-001',
180
- total: 100,
181
- status: 'pending',
182
- createdAt: date
183
- });
184
-
185
- const component = entity.getInMemory(TestOrder);
186
- const data = component?.serializableData();
187
-
188
- expect(data?.createdAt).toBe('2024-01-15T10:30:00.000Z');
189
- });
190
-
191
- test('serializableData throws descriptive error on invalid Date', () => {
192
- const entity = new Entity();
193
- entity.add(TestOrder, {
194
- orderNumber: 'ORD-002',
195
- total: 50,
196
- status: 'pending',
197
- createdAt: new Date('not-a-date')
198
- });
199
-
200
- const component = entity.getInMemory(TestOrder);
201
- expect(() => component?.serializableData()).toThrow(
202
- /Invalid Date for property 'createdAt' on component 'TestOrder'/
203
- );
204
- });
205
-
206
- test('serializableData throws on Date type mismatch', () => {
207
- const entity = new Entity();
208
- entity.add(TestOrder, {
209
- orderNumber: 'ORD-003',
210
- total: 50,
211
- status: 'pending',
212
- createdAt: '2024-01-15' as any
213
- });
214
-
215
- const component = entity.getInMemory(TestOrder);
216
- expect(() => component?.serializableData()).toThrow(
217
- /Type mismatch for property 'createdAt' on component 'TestOrder': expected Date, got string/
218
- );
219
- });
220
-
221
- test('serializableData throws on NaN number', () => {
222
- const entity = new Entity();
223
- entity.add(TestOrder, {
224
- orderNumber: 'ORD-004',
225
- total: NaN,
226
- status: 'pending',
227
- createdAt: new Date()
228
- });
229
-
230
- const component = entity.getInMemory(TestOrder);
231
- expect(() => component?.serializableData()).toThrow(
232
- /Invalid number for property 'total' on component 'TestOrder'/
233
- );
234
- });
235
-
236
- test('serializableData throws on Infinity number', () => {
237
- const entity = new Entity();
238
- entity.add(TestOrder, {
239
- orderNumber: 'ORD-005',
240
- total: Infinity,
241
- status: 'pending',
242
- createdAt: new Date()
243
- });
244
-
245
- const component = entity.getInMemory(TestOrder);
246
- expect(() => component?.serializableData()).toThrow(
247
- /Invalid number for property 'total' on component 'TestOrder'/
248
- );
249
- });
250
-
251
- test('serializableData allows null/undefined for nullable Date/Number', () => {
252
- const entity = new Entity();
253
- entity.add(TestOrder, {
254
- orderNumber: 'ORD-006',
255
- total: 0,
256
- status: 'pending',
257
- createdAt: null as any
258
- });
259
-
260
- const component = entity.getInMemory(TestOrder);
261
- expect(() => component?.serializableData()).not.toThrow();
262
- });
263
- });
264
-
265
- describe('component state', () => {
266
- test('new component is not persisted', () => {
267
- const entity = new Entity();
268
- entity.add(TestUser, { name: 'John', email: 'john@test.com', age: 30 });
269
-
270
- const component = entity.getInMemory(TestUser);
271
- expect((component as any)._persisted).toBe(false);
272
- });
273
-
274
- test('new component is dirty', () => {
275
- const entity = new Entity();
276
- entity.add(TestUser, { name: 'John', email: 'john@test.com', age: 30 });
277
-
278
- const component = entity.getInMemory(TestUser);
279
- expect((component as any)._dirty).toBe(false); // Initial state from constructor
280
- });
281
-
282
- test('component has type ID', () => {
283
- const entity = new Entity();
284
- entity.add(TestUser, { name: 'John', email: 'john@test.com', age: 30 });
285
-
286
- const component = entity.getInMemory(TestUser);
287
- expect(component?.getTypeID()).toBeDefined();
288
- expect(component?.getTypeID().length).toBeGreaterThan(0);
289
- });
290
-
291
- test('component properties returns correct list', () => {
292
- const entity = new Entity();
293
- entity.add(TestUser, { name: 'John', email: 'john@test.com', age: 30 });
294
-
295
- const component = entity.getInMemory(TestUser);
296
- const props = component?.properties();
297
-
298
- expect(props).toContain('name');
299
- expect(props).toContain('email');
300
- expect(props).toContain('age');
301
- expect(props).toContain('bio');
302
- });
303
-
304
- test('indexedProperties returns only indexed fields', () => {
305
- const entity = new Entity();
306
- entity.add(TestUser, { name: 'John', email: 'john@test.com', age: 30 });
307
-
308
- const component = entity.getInMemory(TestUser);
309
- const indexed = component?.indexedProperties();
310
-
311
- expect(indexed).toContain('name');
312
- expect(indexed).toContain('email');
313
- expect(indexed).not.toContain('age');
314
- expect(indexed).not.toContain('bio');
315
- });
316
- });
317
- });
@@ -1,51 +0,0 @@
1
- /**
2
- * BUNSANE-001 defensive harness: verify Entity.drainPendingSideEffects()
3
- * awaits post-commit work scheduled via queueMicrotask from save(), so
4
- * tests under PGlite can settle prior-file background work before
5
- * asserting against freshly-committed state.
6
- */
7
- import { describe, test, expect, beforeAll } from 'bun:test';
8
- import { Entity } from '../../../core/Entity';
9
- import { BaseComponent } from '../../../core/components/BaseComponent';
10
- import { Component, CompData } from '../../../core/components/Decorators';
11
- import { ensureComponentsRegistered } from '../../utils';
12
-
13
- @Component
14
- class DrainMarker extends BaseComponent {
15
- @CompData()
16
- value: string = '';
17
- }
18
-
19
- describe('Entity.drainPendingSideEffects', () => {
20
- beforeAll(async () => {
21
- await ensureComponentsRegistered(DrainMarker);
22
- });
23
-
24
- test('no-op when nothing is pending', async () => {
25
- await expect(Entity.drainPendingSideEffects(100)).resolves.toBeUndefined();
26
- });
27
-
28
- test('awaits post-commit side effects scheduled by save()', async () => {
29
- const saved = Entity.Create();
30
- saved.add(DrainMarker, { value: 'pending' });
31
- await saved.save();
32
-
33
- // runPostCommitSideEffects is queued as a microtask. drain() must
34
- // settle it before returning.
35
- await Entity.drainPendingSideEffects(2_000);
36
-
37
- // A second drain is a no-op.
38
- await Entity.drainPendingSideEffects(100);
39
- });
40
-
41
- test('bounded by timeout, returns even if drain exceeds it', async () => {
42
- const saved = Entity.Create();
43
- saved.add(DrainMarker, { value: 'bounded' });
44
- await saved.save();
45
-
46
- const start = Date.now();
47
- await Entity.drainPendingSideEffects(1);
48
- const elapsed = Date.now() - start;
49
- expect(elapsed).toBeLessThan(500);
50
- });
51
- });
@@ -1,63 +0,0 @@
1
- /**
2
- * Unit tests for Entity.reload (BUNSANE-006).
3
- * Ensures in-memory component state is discarded and re-hydrated from DB.
4
- */
5
- import { describe, test, expect, beforeAll } from 'bun:test';
6
- import { Entity } from '../../../core/Entity';
7
- import { BaseComponent } from '../../../core/components/BaseComponent';
8
- import { Component, CompData } from '../../../core/components/Decorators';
9
- import { ensureComponentsRegistered } from '../../utils';
10
- import db from '../../../database';
11
-
12
- @Component
13
- class ReloadStatus extends BaseComponent {
14
- @CompData()
15
- value: string = '';
16
- }
17
-
18
- describe('Entity.reload', () => {
19
- beforeAll(async () => {
20
- await ensureComponentsRegistered(ReloadStatus);
21
- });
22
-
23
- test('no-op on entity without a valid id', async () => {
24
- const entity = new Entity('');
25
- await expect(entity.reload()).resolves.toBe(entity);
26
- });
27
-
28
- test('refreshes in-memory data after raw-SQL write', async () => {
29
- const saved = Entity.Create();
30
- saved.add(ReloadStatus, { value: 'before' });
31
- await saved.save();
32
-
33
- const typeId = new ReloadStatus().getTypeID();
34
-
35
- // Write new value via raw SQL — bypasses entity cache invalidation.
36
- await db.unsafe(
37
- `UPDATE components SET data = data || '{"value":"after"}'::jsonb
38
- WHERE entity_id = $1 AND type_id = $2`,
39
- [saved.id, typeId]
40
- );
41
-
42
- // In-memory copy still holds stale value.
43
- expect(saved.getInMemory(ReloadStatus)?.value).toBe('before');
44
-
45
- const returned = await saved.reload();
46
- expect(returned).toBe(saved);
47
- expect(saved.getInMemory(ReloadStatus)?.value).toBe('after');
48
- });
49
-
50
- test('hydrates a bare Entity instance from DB', async () => {
51
- const saved = Entity.Create();
52
- saved.add(ReloadStatus, { value: 'hydrated' });
53
- await saved.save();
54
-
55
- const bare = new Entity(saved.id);
56
- expect(bare.componentList().length).toBe(0);
57
-
58
- await bare.reload();
59
-
60
- expect(bare.hasInMemory(ReloadStatus)).toBe(true);
61
- expect(bare.getInMemory(ReloadStatus)?.value).toBe('hydrated');
62
- });
63
- });
@@ -1,72 +0,0 @@
1
- /**
2
- * Unit tests for Entity.requireComponents (BUNSANE-003).
3
- * Ensures ComponentTargetHook includeComponents matching sees tag
4
- * components that weren't eagerly loaded.
5
- */
6
- import { describe, test, expect, beforeAll } from 'bun:test';
7
- import { Entity } from '../../../core/Entity';
8
- import { BaseComponent } from '../../../core/components/BaseComponent';
9
- import { Component, CompData } from '../../../core/components/Decorators';
10
- import { TestUser } from '../../fixtures/components';
11
- import { ensureComponentsRegistered } from '../../utils';
12
-
13
- @Component
14
- class ReqTag extends BaseComponent {}
15
-
16
- @Component
17
- class ReqData extends BaseComponent {
18
- @CompData()
19
- value: string = '';
20
- }
21
-
22
- describe('Entity.requireComponents', () => {
23
- beforeAll(async () => {
24
- await ensureComponentsRegistered(TestUser, ReqTag, ReqData);
25
- });
26
-
27
- test('no-op for empty list', async () => {
28
- const entity = Entity.Create();
29
- await expect(entity.requireComponents([])).resolves.toBeUndefined();
30
- expect(entity.componentList().length).toBe(0);
31
- });
32
-
33
- test('does nothing when components already in memory', async () => {
34
- const entity = Entity.Create();
35
- entity.add(ReqTag);
36
- const before = entity.componentList().length;
37
- await entity.requireComponents([ReqTag]);
38
- expect(entity.componentList().length).toBe(before);
39
- });
40
-
41
- test('hydrates missing components from DB after save', async () => {
42
- const saved = Entity.Create();
43
- saved.add(ReqTag);
44
- saved.add(ReqData, { value: 'hello' });
45
- await saved.save();
46
-
47
- const loaded = new Entity(saved.id);
48
- expect(loaded.componentList().length).toBe(0);
49
-
50
- await loaded.requireComponents([ReqTag, ReqData]);
51
-
52
- expect(loaded.hasInMemory(ReqTag)).toBe(true);
53
- expect(loaded.hasInMemory(ReqData)).toBe(true);
54
- expect(loaded.getInMemory(ReqData)?.value).toBe('hello');
55
- });
56
-
57
- test('only fetches missing components, not already-loaded ones', async () => {
58
- const saved = Entity.Create();
59
- saved.add(ReqTag);
60
- saved.add(ReqData, { value: 'mix' });
61
- await saved.save();
62
-
63
- const loaded = new Entity(saved.id);
64
- await loaded.requireComponents([ReqData]);
65
- expect(loaded.hasInMemory(ReqData)).toBe(true);
66
- expect(loaded.hasInMemory(ReqTag)).toBe(false);
67
-
68
- await loaded.requireComponents([ReqTag, ReqData]);
69
- expect(loaded.hasInMemory(ReqTag)).toBe(true);
70
- expect(loaded.getInMemory(ReqData)?.value).toBe('mix');
71
- });
72
- });