bunsane 0.3.2 → 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 (214) hide show
  1. package/CHANGELOG.md +445 -370
  2. package/core/BatchLoader.ts +56 -32
  3. package/core/Entity.ts +85 -1020
  4. package/core/EntityHookManager.ts +52 -754
  5. package/core/Logger.ts +10 -0
  6. package/core/RequestContext.ts +94 -85
  7. package/core/RequestLoaders.ts +98 -5
  8. package/core/SchedulerManager.ts +28 -600
  9. package/core/app/cors.ts +2 -11
  10. package/core/app/preparedStatementWarmup.ts +9 -49
  11. package/core/app/requestRouter.ts +9 -8
  12. package/core/app/restRegistry.ts +8 -0
  13. package/core/archetype/fieldResolvers.ts +85 -40
  14. package/core/archetype/relationLoader.ts +135 -92
  15. package/core/cache/CacheManager.ts +91 -302
  16. package/core/cache/CompressionUtils.ts +34 -3
  17. package/core/cache/MemoryCache.ts +40 -37
  18. package/core/cache/RedisCache.ts +4 -4
  19. package/core/cache/health.ts +30 -0
  20. package/core/cache/invalidation.ts +96 -0
  21. package/core/cache/strategies/writeInvalidate.ts +111 -0
  22. package/core/cache/strategies/writeThrough.ts +233 -0
  23. package/core/components/BaseComponent.ts +16 -8
  24. package/core/components/ComponentRegistry.ts +28 -0
  25. package/core/decorators/IndexedField.ts +1 -1
  26. package/core/entity/cacheStrategies.ts +97 -0
  27. package/core/entity/componentAccess.ts +364 -0
  28. package/core/entity/finders.ts +202 -0
  29. package/core/entity/pendingOps.ts +72 -0
  30. package/core/entity/saveEntity.ts +377 -0
  31. package/core/hooks/dispatcher.ts +439 -0
  32. package/core/hooks/guards.ts +155 -0
  33. package/core/hooks/registry.ts +247 -0
  34. package/core/metadata/definitions/Component.ts +1 -1
  35. package/core/metadata/index.ts +15 -4
  36. package/core/middleware/RateLimit.ts +102 -105
  37. package/core/middleware/RequestId.ts +2 -9
  38. package/core/middleware/SecurityHeaders.ts +2 -11
  39. package/core/middleware/headers.ts +28 -0
  40. package/core/remote/OutboxWorker.ts +213 -183
  41. package/core/remote/RemoteManager.ts +401 -400
  42. package/core/remote/types.ts +153 -151
  43. package/core/requestScope.ts +34 -0
  44. package/core/scheduler/cronEvaluator.ts +174 -0
  45. package/core/scheduler/lifecycleHooks.ts +21 -0
  46. package/core/scheduler/lockCoordinator.ts +27 -0
  47. package/core/scheduler/metrics.ts +14 -0
  48. package/core/scheduler/taskRunner.ts +420 -0
  49. package/database/DatabaseHelper.ts +128 -101
  50. package/database/IndexingStrategy.ts +72 -2
  51. package/database/PreparedStatementCache.ts +8 -2
  52. package/database/cancellable.ts +35 -22
  53. package/database/index.ts +15 -3
  54. package/database/instrumentedDb.ts +141 -141
  55. package/endpoints/archetypes.ts +2 -8
  56. package/endpoints/tables.ts +6 -1
  57. package/gql/index.ts +1 -1
  58. package/gql/visitors/ResolverGeneratorVisitor.ts +25 -4
  59. package/package.json +22 -1
  60. package/query/CTENode.ts +5 -3
  61. package/query/ComponentInclusionNode.ts +240 -13
  62. package/query/OrNode.ts +6 -5
  63. package/query/Query.ts +157 -46
  64. package/query/QueryContext.ts +6 -0
  65. package/query/QueryDAG.ts +7 -2
  66. package/query/membershipSource.ts +66 -0
  67. package/storage/LocalStorageProvider.ts +8 -3
  68. package/studio/dist/assets/index-BMZ67Npg.js +254 -0
  69. package/studio/dist/assets/index-BpbuYz9g.css +1 -0
  70. package/studio/{index.html → dist/index.html} +3 -2
  71. package/swagger/generator.ts +11 -1
  72. package/upload/UploadManager.ts +8 -6
  73. package/utils/uuid.ts +40 -10
  74. package/.claude/scheduled_tasks.lock +0 -1
  75. package/.claude/settings.local.json +0 -47
  76. package/.prettierrc +0 -4
  77. package/.serena/memories/architectural-decision-no-dependency-injection.md +0 -76
  78. package/.serena/memories/architecture.md +0 -154
  79. package/.serena/memories/cache-interface-refactoring-2026-01-24.md +0 -165
  80. package/.serena/memories/code_style_and_conventions.md +0 -76
  81. package/.serena/memories/project_overview.md +0 -43
  82. package/.serena/memories/schema-dsl-plan.md +0 -107
  83. package/.serena/memories/suggested_commands.md +0 -80
  84. package/.serena/memories/typescript-compilation-status.md +0 -54
  85. package/.serena/project.yml +0 -114
  86. package/BunSane.jpg +0 -0
  87. package/CLAUDE.md +0 -198
  88. package/TODO.md +0 -2
  89. package/bun.lock +0 -302
  90. package/bunfig.toml +0 -10
  91. package/docs/RFC_APP_REFACTOR.md +0 -248
  92. package/docs/RFC_REFACTOR_TARGETS.md +0 -251
  93. package/docs/SCALABILITY_PLAN.md +0 -175
  94. package/studio/bun.lock +0 -482
  95. package/studio/package.json +0 -39
  96. package/studio/postcss.config.js +0 -6
  97. package/studio/src/components/DataTable.tsx +0 -211
  98. package/studio/src/components/Layout.tsx +0 -13
  99. package/studio/src/components/PageContainer.tsx +0 -9
  100. package/studio/src/components/PageHeader.tsx +0 -13
  101. package/studio/src/components/SearchBar.tsx +0 -57
  102. package/studio/src/components/Sidebar.tsx +0 -294
  103. package/studio/src/components/ui/button.tsx +0 -56
  104. package/studio/src/components/ui/checkbox.tsx +0 -26
  105. package/studio/src/components/ui/input.tsx +0 -25
  106. package/studio/src/hooks/useDataTable.ts +0 -131
  107. package/studio/src/index.css +0 -36
  108. package/studio/src/lib/api.ts +0 -186
  109. package/studio/src/lib/utils.ts +0 -13
  110. package/studio/src/main.tsx +0 -17
  111. package/studio/src/pages/ArcheType.tsx +0 -239
  112. package/studio/src/pages/Components.tsx +0 -124
  113. package/studio/src/pages/EntityInspector.tsx +0 -302
  114. package/studio/src/pages/QueryRunner.tsx +0 -246
  115. package/studio/src/pages/Table.tsx +0 -94
  116. package/studio/src/pages/Welcome.tsx +0 -241
  117. package/studio/src/routes.tsx +0 -45
  118. package/studio/src/store/archeTypeSettings.ts +0 -30
  119. package/studio/src/store/studio.ts +0 -65
  120. package/studio/src/utils/columnHelpers.tsx +0 -114
  121. package/studio/studio-instructions.md +0 -81
  122. package/studio/tailwind.config.js +0 -77
  123. package/studio/utils.ts +0 -54
  124. package/studio/vite.config.js +0 -19
  125. package/tests/benchmark/BENCHMARK_DATABASES_PLAN.md +0 -338
  126. package/tests/benchmark/bunfig.toml +0 -9
  127. package/tests/benchmark/fixtures/EcommerceComponents.ts +0 -283
  128. package/tests/benchmark/fixtures/EcommerceDataGenerators.ts +0 -301
  129. package/tests/benchmark/fixtures/RelationTracker.ts +0 -159
  130. package/tests/benchmark/fixtures/index.ts +0 -6
  131. package/tests/benchmark/index.ts +0 -22
  132. package/tests/benchmark/noop-preload.ts +0 -3
  133. package/tests/benchmark/query-lateral-benchmark.test.ts +0 -372
  134. package/tests/benchmark/runners/BenchmarkLoader.ts +0 -132
  135. package/tests/benchmark/runners/index.ts +0 -4
  136. package/tests/benchmark/scenarios/query-benchmarks.test.ts +0 -465
  137. package/tests/benchmark/scripts/generate-db.ts +0 -344
  138. package/tests/benchmark/scripts/run-benchmarks.ts +0 -97
  139. package/tests/e2e/http.test.ts +0 -130
  140. package/tests/fixtures/archetypes/TestUserArchetype.ts +0 -21
  141. package/tests/fixtures/components/TestOrder.ts +0 -23
  142. package/tests/fixtures/components/TestProduct.ts +0 -23
  143. package/tests/fixtures/components/TestUser.ts +0 -20
  144. package/tests/fixtures/components/index.ts +0 -6
  145. package/tests/graphql/SchemaGeneration.test.ts +0 -90
  146. package/tests/graphql/builders/ResolverBuilder.test.ts +0 -223
  147. package/tests/graphql/builders/TypeDefBuilder.test.ts +0 -153
  148. package/tests/helpers/MockRedisClient.ts +0 -113
  149. package/tests/helpers/MockRedisStreamServer.ts +0 -448
  150. package/tests/integration/archetype/ArcheType.persistence.test.ts +0 -241
  151. package/tests/integration/cache/CacheInvalidation.test.ts +0 -259
  152. package/tests/integration/entity/Entity.persistence.test.ts +0 -333
  153. package/tests/integration/entity/Entity.saveTimeout.test.ts +0 -110
  154. package/tests/integration/loaders/RequestLoaders.abort.test.ts +0 -82
  155. package/tests/integration/query/Query.abort.test.ts +0 -66
  156. package/tests/integration/query/Query.complexAnalysis.test.ts +0 -557
  157. package/tests/integration/query/Query.edgeCases.test.ts +0 -595
  158. package/tests/integration/query/Query.exec.test.ts +0 -576
  159. package/tests/integration/query/Query.explainAnalyze.test.ts +0 -233
  160. package/tests/integration/query/Query.jsonbArray.test.ts +0 -214
  161. package/tests/integration/remote/dlq.test.ts +0 -175
  162. package/tests/integration/remote/event-dispatch.test.ts +0 -114
  163. package/tests/integration/remote/outbox.test.ts +0 -130
  164. package/tests/integration/remote/rpc.test.ts +0 -177
  165. package/tests/pglite-setup.ts +0 -62
  166. package/tests/setup.ts +0 -164
  167. package/tests/stress/BenchmarkRunner.ts +0 -203
  168. package/tests/stress/DataSeeder.ts +0 -190
  169. package/tests/stress/StressTestReporter.ts +0 -229
  170. package/tests/stress/cursor-perf-test.ts +0 -171
  171. package/tests/stress/fixtures/RealisticComponents.ts +0 -235
  172. package/tests/stress/fixtures/StressTestComponents.ts +0 -58
  173. package/tests/stress/index.ts +0 -7
  174. package/tests/stress/scenarios/query-benchmarks.test.ts +0 -285
  175. package/tests/stress/scenarios/realistic-scenarios.test.ts +0 -1081
  176. package/tests/stress/scenarios/timeout-investigation.test.ts +0 -522
  177. package/tests/unit/BatchLoader.test.ts +0 -196
  178. package/tests/unit/archetype/ArcheType.test.ts +0 -107
  179. package/tests/unit/cache/CacheManager.test.ts +0 -498
  180. package/tests/unit/cache/MemoryCache.test.ts +0 -260
  181. package/tests/unit/cache/RedisCache.test.ts +0 -411
  182. package/tests/unit/database/cancellable.test.ts +0 -81
  183. package/tests/unit/database/instrumentedDb.test.ts +0 -160
  184. package/tests/unit/entity/Entity.components.test.ts +0 -317
  185. package/tests/unit/entity/Entity.drainSideEffects.test.ts +0 -51
  186. package/tests/unit/entity/Entity.reload.test.ts +0 -63
  187. package/tests/unit/entity/Entity.requireComponents.test.ts +0 -72
  188. package/tests/unit/entity/Entity.test.ts +0 -345
  189. package/tests/unit/gql/depthLimit.test.ts +0 -203
  190. package/tests/unit/gql/operationMiddleware.test.ts +0 -293
  191. package/tests/unit/health/Health.test.ts +0 -129
  192. package/tests/unit/middleware/AccessLog.test.ts +0 -37
  193. package/tests/unit/middleware/Middleware.test.ts +0 -98
  194. package/tests/unit/middleware/RequestId.test.ts +0 -54
  195. package/tests/unit/middleware/SecurityHeaders.test.ts +0 -66
  196. package/tests/unit/query/FilterBuilder.test.ts +0 -111
  197. package/tests/unit/query/JsonbArrayBuilder.test.ts +0 -178
  198. package/tests/unit/query/Query.emptyString.test.ts +0 -69
  199. package/tests/unit/query/Query.test.ts +0 -310
  200. package/tests/unit/remote/CircuitBreaker.test.ts +0 -159
  201. package/tests/unit/remote/RemoteError.test.ts +0 -55
  202. package/tests/unit/remote/decorators.test.ts +0 -195
  203. package/tests/unit/remote/metrics.test.ts +0 -115
  204. package/tests/unit/remote/mockRedisStreamServer.test.ts +0 -104
  205. package/tests/unit/scheduler/DistributedLock.test.ts +0 -274
  206. package/tests/unit/scheduler/SchedulerManager.timeBased.test.ts +0 -95
  207. package/tests/unit/schema/schema-integration.test.ts +0 -426
  208. package/tests/unit/schema/schema.test.ts +0 -580
  209. package/tests/unit/storage/S3StorageProvider.test.ts +0 -567
  210. package/tests/unit/upload/RestUpload.test.ts +0 -267
  211. package/tests/unit/validateEnv.test.ts +0 -82
  212. package/tests/utils/entity-tracker.ts +0 -57
  213. package/tests/utils/index.ts +0 -13
  214. package/tests/utils/test-context.ts +0 -149
@@ -1,576 +0,0 @@
1
- /**
2
- * Integration tests for Query execution
3
- * Tests query execution against the database
4
- */
5
- import { describe, test, expect, beforeAll, beforeEach, afterEach } from 'bun:test';
6
- import { Entity } from '../../../core/Entity';
7
- import { Query, FilterOp } from '../../../query/Query';
8
- import { TestUser, TestProduct, TestOrder } from '../../fixtures/components';
9
- import { createTestContext, ensureComponentsRegistered } from '../../utils';
10
-
11
- describe('Query Execution', () => {
12
- const ctx = createTestContext();
13
-
14
- beforeAll(async () => {
15
- await ensureComponentsRegistered(TestUser, TestProduct, TestOrder);
16
- });
17
-
18
- describe('basic query execution', () => {
19
- test('exec() returns only entities with specified component', async () => {
20
- // Positive case: entity WITH TestUser component
21
- const withUser = ctx.tracker.create();
22
- withUser.add(TestUser, { name: 'QueryTest', email: 'query@example.com', age: 30 });
23
- await withUser.save();
24
-
25
- // Negative case: entity WITHOUT TestUser component (only has TestProduct)
26
- const withoutUser = ctx.tracker.create();
27
- withoutUser.add(TestProduct, { sku: 'NO_USER', name: 'No User Product', price: 10, inStock: true });
28
- await withoutUser.save();
29
-
30
- const results = await new Query()
31
- .with(TestUser)
32
- .exec();
33
-
34
- // Positive: entity with TestUser should be found
35
- const foundWithUser = results.some(e => e.id === withUser.id);
36
- expect(foundWithUser).toBe(true);
37
-
38
- // Negative: entity without TestUser should NOT be found
39
- const foundWithoutUser = results.some(e => e.id === withoutUser.id);
40
- expect(foundWithoutUser).toBe(false);
41
- });
42
-
43
- test('populate() loads all component data', async () => {
44
- const entity = ctx.tracker.create();
45
- entity.add(TestUser, { name: 'PopulateTest', email: 'populate@example.com', age: 25 });
46
- await entity.save();
47
-
48
- const results = await new Query()
49
- .with(TestUser)
50
- .populate()
51
- .exec();
52
-
53
- const found = results.find(e => e.id === entity.id);
54
- expect(found).toBeDefined();
55
- expect(found?.getInMemory(TestUser)).toBeDefined();
56
- });
57
-
58
- test('returns empty array when no matches', async () => {
59
- // Query for entities with a unique component
60
- const results = await new Query()
61
- .with(TestUser, {
62
- filters: [Query.filter('email', FilterOp.EQ, 'definitely-does-not-exist@nowhere.com')]
63
- })
64
- .exec();
65
-
66
- expect(results.length).toBe(0);
67
- });
68
- });
69
-
70
- describe('findById()', () => {
71
- test('finds entity by ID', async () => {
72
- const entity = ctx.tracker.create();
73
- entity.add(TestUser, { name: 'FindById', email: 'findby@example.com', age: 35 });
74
- await entity.save();
75
-
76
- const results = await new Query()
77
- .findById(entity.id)
78
- .exec();
79
-
80
- expect(results.length).toBe(1);
81
- expect(results[0]!.id).toBe(entity.id);
82
- });
83
-
84
- test('findOneById returns single entity', async () => {
85
- const entity = ctx.tracker.create();
86
- entity.add(TestUser, { name: 'FindOne', email: 'findone@example.com', age: 40 });
87
- await entity.save();
88
-
89
- const result = await new Query().findOneById(entity.id);
90
-
91
- expect(result).not.toBeNull();
92
- expect(result?.id).toBe(entity.id);
93
- });
94
-
95
- test('findOneById returns null for non-existent ID', async () => {
96
- const result = await new Query().findOneById('00000000-0000-0000-0000-000000000000');
97
- expect(result).toBeNull();
98
- });
99
- });
100
-
101
- describe('filtering', () => {
102
- beforeEach(async () => {
103
- // Create test data
104
- const entity1 = ctx.tracker.create();
105
- entity1.add(TestUser, { name: 'Alice', email: 'alice@example.com', age: 25 });
106
- await entity1.save();
107
-
108
- const entity2 = ctx.tracker.create();
109
- entity2.add(TestUser, { name: 'Bob', email: 'bob@example.com', age: 35 });
110
- await entity2.save();
111
-
112
- const entity3 = ctx.tracker.create();
113
- entity3.add(TestUser, { name: 'Charlie', email: 'charlie@example.com', age: 45 });
114
- await entity3.save();
115
- });
116
-
117
- test('EQ filter finds exact match', async () => {
118
- const results = await new Query()
119
- .with(TestUser, {
120
- filters: [Query.filter('name', FilterOp.EQ, 'Alice')]
121
- })
122
- .populate()
123
- .exec();
124
-
125
- const alice = results.find(e => e.getInMemory(TestUser)?.name === 'Alice');
126
- expect(alice).toBeDefined();
127
- });
128
-
129
- test('GT filter finds greater values', async () => {
130
- const results = await new Query()
131
- .with(TestUser, {
132
- filters: [Query.filter('age', FilterOp.GT, 30)]
133
- })
134
- .populate()
135
- .exec();
136
-
137
- for (const entity of results) {
138
- const user = entity.getInMemory(TestUser);
139
- if (user) {
140
- expect(user.age).toBeGreaterThan(30);
141
- }
142
- }
143
- });
144
-
145
- test('LT filter finds lesser values', async () => {
146
- const results = await new Query()
147
- .with(TestUser, {
148
- filters: [Query.filter('age', FilterOp.LT, 40)]
149
- })
150
- .populate()
151
- .exec();
152
-
153
- for (const entity of results) {
154
- const user = entity.getInMemory(TestUser);
155
- if (user) {
156
- expect(user.age).toBeLessThan(40);
157
- }
158
- }
159
- });
160
-
161
- test('LIKE filter finds partial matches', async () => {
162
- const results = await new Query()
163
- .with(TestUser, {
164
- filters: [Query.filter('email', FilterOp.LIKE, '%example.com')]
165
- })
166
- .populate()
167
- .exec();
168
-
169
- expect(results.length).toBeGreaterThanOrEqual(1);
170
- });
171
- });
172
-
173
- describe('pagination', () => {
174
- beforeEach(async () => {
175
- // Create multiple entities for pagination
176
- for (let i = 0; i < 5; i++) {
177
- const entity = ctx.tracker.create();
178
- entity.add(TestUser, {
179
- name: `PaginationUser${i}`,
180
- email: `page${i}@example.com`,
181
- age: 20 + i
182
- });
183
- await entity.save();
184
- }
185
- });
186
-
187
- test('take() limits results', async () => {
188
- const results = await new Query()
189
- .with(TestUser)
190
- .take(2)
191
- .exec();
192
-
193
- expect(results.length).toBeLessThanOrEqual(2);
194
- });
195
-
196
- test('offset() skips results', async () => {
197
- const allResults = await new Query()
198
- .with(TestUser)
199
- .exec();
200
-
201
- const offsetResults = await new Query()
202
- .with(TestUser)
203
- .offset(2)
204
- .take(100)
205
- .exec();
206
-
207
- expect(offsetResults.length).toBe(Math.max(0, allResults.length - 2));
208
- });
209
-
210
- test('take() and offset() work together', async () => {
211
- const results = await new Query()
212
- .with(TestUser)
213
- .take(2)
214
- .offset(1)
215
- .exec();
216
-
217
- expect(results.length).toBeLessThanOrEqual(2);
218
- });
219
- });
220
-
221
- describe('sorting', () => {
222
- beforeEach(async () => {
223
- const ages = [30, 20, 40, 25, 35];
224
- for (let i = 0; i < ages.length; i++) {
225
- const entity = ctx.tracker.create();
226
- entity.add(TestUser, {
227
- name: `SortUser${i}`,
228
- email: `sort${i}@example.com`,
229
- age: ages[i]!
230
- });
231
- await entity.save();
232
- }
233
- });
234
-
235
- test('sortBy ASC orders correctly', async () => {
236
- const results = await new Query()
237
- .with(TestUser, {
238
- filters: [Query.filter('email', FilterOp.LIKE, 'sort%@example.com')]
239
- })
240
- .sortBy(TestUser, 'age', 'ASC')
241
- .populate()
242
- .exec();
243
-
244
- for (let i = 1; i < results.length; i++) {
245
- const prevAge = results[i - 1]!.getInMemory(TestUser)?.age ?? 0;
246
- const currAge = results[i]!.getInMemory(TestUser)?.age ?? 0;
247
- expect(currAge).toBeGreaterThanOrEqual(prevAge);
248
- }
249
- });
250
-
251
- test('sortBy DESC orders correctly', async () => {
252
- const results = await new Query()
253
- .with(TestUser, {
254
- filters: [Query.filter('email', FilterOp.LIKE, 'sort%@example.com')]
255
- })
256
- .sortBy(TestUser, 'age', 'DESC')
257
- .populate()
258
- .exec();
259
-
260
- for (let i = 1; i < results.length; i++) {
261
- const prevAge = results[i - 1]!.getInMemory(TestUser)?.age ?? 0;
262
- const currAge = results[i]!.getInMemory(TestUser)?.age ?? 0;
263
- expect(currAge).toBeLessThanOrEqual(prevAge);
264
- }
265
- });
266
- });
267
-
268
- describe('count()', () => {
269
- test('returns count of matching entities', async () => {
270
- const entity = ctx.tracker.create();
271
- entity.add(TestUser, { name: 'CountTest', email: 'count@example.com', age: 30 });
272
- await entity.save();
273
-
274
- const count = await new Query()
275
- .with(TestUser)
276
- .count();
277
-
278
- expect(count).toBeGreaterThanOrEqual(1);
279
- });
280
-
281
- test('count respects filters', async () => {
282
- const entity = ctx.tracker.create();
283
- entity.add(TestUser, { name: 'UniqueCountName', email: 'uniquecount@example.com', age: 99 });
284
- await entity.save();
285
-
286
- const count = await new Query()
287
- .with(TestUser, {
288
- filters: [Query.filter('name', FilterOp.EQ, 'UniqueCountName')]
289
- })
290
- .count();
291
-
292
- expect(count).toBe(1);
293
- });
294
- });
295
-
296
- describe('multiple components', () => {
297
- test('with() multiple components finds only entities with ALL components', async () => {
298
- // Positive case: entity with BOTH TestUser AND TestProduct
299
- const withBoth = ctx.tracker.create();
300
- withBoth.add(TestUser, { name: 'MultiComp', email: 'multi@example.com', age: 30 });
301
- withBoth.add(TestProduct, { sku: 'MULTI', name: 'Multi Product', price: 50, inStock: true });
302
- await withBoth.save();
303
-
304
- // Negative case: entity with ONLY TestUser (missing TestProduct)
305
- const withOnlyUser = ctx.tracker.create();
306
- withOnlyUser.add(TestUser, { name: 'OnlyUser', email: 'onlyuser@example.com', age: 25 });
307
- await withOnlyUser.save();
308
-
309
- const results = await new Query()
310
- .with(TestUser)
311
- .with(TestProduct)
312
- .populate()
313
- .exec();
314
-
315
- // Positive: entity with both components should be found
316
- const foundWithBoth = results.find(e => e.id === withBoth.id);
317
- expect(foundWithBoth).toBeDefined();
318
- expect(foundWithBoth?.getInMemory(TestUser)).toBeDefined();
319
- expect(foundWithBoth?.getInMemory(TestProduct)).toBeDefined();
320
-
321
- // Negative: entity with only one component should NOT be found
322
- const foundWithOnlyUser = results.some(e => e.id === withOnlyUser.id);
323
- expect(foundWithOnlyUser).toBe(false);
324
- });
325
-
326
- test('without() excludes entities with component', async () => {
327
- const withProduct = ctx.tracker.create();
328
- withProduct.add(TestUser, { name: 'WithProduct', email: 'withprod@example.com', age: 25 });
329
- withProduct.add(TestProduct, { sku: 'WITH', name: 'With', price: 10, inStock: true });
330
- await withProduct.save();
331
-
332
- const withoutProduct = ctx.tracker.create();
333
- withoutProduct.add(TestUser, { name: 'WithoutProduct', email: 'withoutprod@example.com', age: 30 });
334
- await withoutProduct.save();
335
-
336
- const results = await new Query()
337
- .with(TestUser)
338
- .without(TestProduct)
339
- .exec();
340
-
341
- const hasWithProduct = results.some(e => e.id === withProduct.id);
342
- expect(hasWithProduct).toBe(false);
343
- });
344
-
345
- test('with() 3+ components without filters finds entities with ALL components', async () => {
346
- // Positive case: entity with ALL 3 components
347
- const withAll = ctx.tracker.create();
348
- withAll.add(TestUser, { name: 'AllThree', email: 'allthree@example.com', age: 30 });
349
- withAll.add(TestProduct, { sku: 'ALL3', name: 'All Three Product', price: 100, inStock: true });
350
- withAll.add(TestOrder, { orderNumber: 'ORD-ALL3', total: 100, status: 'pending' });
351
- await withAll.save();
352
-
353
- // Negative case: entity with only 2 of 3 components (missing TestOrder)
354
- const withTwo = ctx.tracker.create();
355
- withTwo.add(TestUser, { name: 'TwoOnly', email: 'twoonly@example.com', age: 25 });
356
- withTwo.add(TestProduct, { sku: 'TWO', name: 'Two Only Product', price: 50, inStock: true });
357
- await withTwo.save();
358
-
359
- // Query for entities with all 3 components - NO FILTERS
360
- const results = await new Query()
361
- .with(TestUser)
362
- .with(TestProduct)
363
- .with(TestOrder)
364
- .exec();
365
-
366
- // Positive: entity with all 3 components should be found
367
- const foundWithAll = results.some(e => e.id === withAll.id);
368
- expect(foundWithAll).toBe(true);
369
-
370
- // Negative: entity with only 2 components should NOT be found
371
- const foundWithTwo = results.some(e => e.id === withTwo.id);
372
- expect(foundWithTwo).toBe(false);
373
- });
374
- });
375
-
376
- describe('excludeEntityId()', () => {
377
- test('excludes specific entity from results', async () => {
378
- const entity1 = ctx.tracker.create();
379
- entity1.add(TestUser, { name: 'Include', email: 'include@example.com', age: 30 });
380
- await entity1.save();
381
-
382
- const entity2 = ctx.tracker.create();
383
- entity2.add(TestUser, { name: 'Exclude', email: 'exclude@example.com', age: 30 });
384
- await entity2.save();
385
-
386
- const results = await new Query()
387
- .with(TestUser)
388
- .excludeEntityId(entity2.id)
389
- .exec();
390
-
391
- const hasExcluded = results.some(e => e.id === entity2.id);
392
- expect(hasExcluded).toBe(false);
393
- });
394
- });
395
-
396
- describe('eagerLoadComponents()', () => {
397
- test('preloads specified components', async () => {
398
- const entity = ctx.tracker.create();
399
- entity.add(TestUser, { name: 'Eager', email: 'eager@example.com', age: 30 });
400
- entity.add(TestProduct, { sku: 'EAGER', name: 'Eager Product', price: 20, inStock: true });
401
- await entity.save();
402
-
403
- const results = await new Query()
404
- .with(TestUser)
405
- .eagerLoadComponents([TestProduct])
406
- .exec();
407
-
408
- const found = results.find(e => e.id === entity.id);
409
- expect(found?.hasInMemory(TestProduct)).toBe(true);
410
- });
411
- });
412
-
413
- describe('sum()', () => {
414
- test('returns correct sum of numeric field', async () => {
415
- // Create entities with known prices
416
- const prices = [10, 20, 30, 40];
417
- const expectedSum = prices.reduce((a, b) => a + b, 0); // 100
418
-
419
- for (let i = 0; i < prices.length; i++) {
420
- const entity = ctx.tracker.create();
421
- entity.add(TestProduct, {
422
- sku: `SUM_TEST_${i}_${Date.now()}`,
423
- name: `Sum Product ${i}`,
424
- price: prices[i]!,
425
- inStock: true
426
- });
427
- await entity.save();
428
- }
429
-
430
- const sum = await new Query()
431
- .with(TestProduct, {
432
- filters: [Query.filter('sku', FilterOp.LIKE, 'SUM_TEST_%')]
433
- })
434
- .sum(TestProduct, 'price');
435
-
436
- expect(sum).toBe(expectedSum);
437
- });
438
-
439
- test('returns 0 when no entities match', async () => {
440
- const sum = await new Query()
441
- .with(TestProduct, {
442
- filters: [Query.filter('sku', FilterOp.EQ, 'NONEXISTENT_SKU_12345')]
443
- })
444
- .sum(TestProduct, 'price');
445
-
446
- expect(sum).toBe(0);
447
- });
448
-
449
- test('sum works with filters', async () => {
450
- // Create entities - some in stock, some not
451
- const entity1 = ctx.tracker.create();
452
- entity1.add(TestProduct, {
453
- sku: `SUM_FILTER_1_${Date.now()}`,
454
- name: 'In Stock',
455
- price: 50,
456
- inStock: true
457
- });
458
- await entity1.save();
459
-
460
- const entity2 = ctx.tracker.create();
461
- entity2.add(TestProduct, {
462
- sku: `SUM_FILTER_2_${Date.now()}`,
463
- name: 'Out of Stock',
464
- price: 100,
465
- inStock: false
466
- });
467
- await entity2.save();
468
-
469
- // Sum only in-stock items
470
- const sum = await new Query()
471
- .with(TestProduct, {
472
- filters: [
473
- Query.filter('sku', FilterOp.LIKE, 'SUM_FILTER_%'),
474
- Query.filter('inStock', FilterOp.EQ, true)
475
- ]
476
- })
477
- .sum(TestProduct, 'price');
478
-
479
- expect(sum).toBe(50);
480
- });
481
- });
482
-
483
- describe('average()', () => {
484
- test('returns correct average of numeric field', async () => {
485
- // Create entities with known prices
486
- const prices = [10, 20, 30, 40];
487
- const expectedAvg = prices.reduce((a, b) => a + b, 0) / prices.length; // 25
488
-
489
- for (let i = 0; i < prices.length; i++) {
490
- const entity = ctx.tracker.create();
491
- entity.add(TestProduct, {
492
- sku: `AVG_TEST_${i}_${Date.now()}`,
493
- name: `Avg Product ${i}`,
494
- price: prices[i]!,
495
- inStock: true
496
- });
497
- await entity.save();
498
- }
499
-
500
- const avg = await new Query()
501
- .with(TestProduct, {
502
- filters: [Query.filter('sku', FilterOp.LIKE, 'AVG_TEST_%')]
503
- })
504
- .average(TestProduct, 'price');
505
-
506
- expect(avg).toBe(expectedAvg);
507
- });
508
-
509
- test('returns 0 when no entities match', async () => {
510
- const avg = await new Query()
511
- .with(TestProduct, {
512
- filters: [Query.filter('sku', FilterOp.EQ, 'NONEXISTENT_SKU_67890')]
513
- })
514
- .average(TestProduct, 'price');
515
-
516
- expect(avg).toBe(0);
517
- });
518
-
519
- test('average handles decimal results', async () => {
520
- // Create entities with prices that result in decimal average
521
- const prices = [10, 20, 30]; // avg = 20
522
-
523
- for (let i = 0; i < prices.length; i++) {
524
- const entity = ctx.tracker.create();
525
- entity.add(TestProduct, {
526
- sku: `AVG_DEC_${i}_${Date.now()}`,
527
- name: `Decimal Avg ${i}`,
528
- price: prices[i]!,
529
- inStock: true
530
- });
531
- await entity.save();
532
- }
533
-
534
- const avg = await new Query()
535
- .with(TestProduct, {
536
- filters: [Query.filter('sku', FilterOp.LIKE, 'AVG_DEC_%')]
537
- })
538
- .average(TestProduct, 'price');
539
-
540
- expect(avg).toBe(20);
541
- });
542
-
543
- test('average works with filters', async () => {
544
- // Create entities - some expensive, some cheap
545
- const entity1 = ctx.tracker.create();
546
- entity1.add(TestProduct, {
547
- sku: `AVG_FILTER_CHEAP_${Date.now()}`,
548
- name: 'Cheap Product',
549
- price: 10,
550
- inStock: true
551
- });
552
- await entity1.save();
553
-
554
- const entity2 = ctx.tracker.create();
555
- entity2.add(TestProduct, {
556
- sku: `AVG_FILTER_EXPENSIVE_${Date.now()}`,
557
- name: 'Expensive Product',
558
- price: 1000,
559
- inStock: true
560
- });
561
- await entity2.save();
562
-
563
- // Average only cheap items (price < 100)
564
- const avg = await new Query()
565
- .with(TestProduct, {
566
- filters: [
567
- Query.filter('sku', FilterOp.LIKE, 'AVG_FILTER_%'),
568
- Query.filter('price', FilterOp.LT, 100)
569
- ]
570
- })
571
- .average(TestProduct, 'price');
572
-
573
- expect(avg).toBe(10);
574
- });
575
- });
576
- });