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,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
- });