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,1081 +0,0 @@
1
- /**
2
- * Realistic Stress Test Scenarios
3
- *
4
- * Simulates real-world e-commerce query patterns with:
5
- * - 1000+ entities seeded with multiple components
6
- * - Multi-filter queries
7
- * - Multi-component joins
8
- * - OR queries
9
- * - Aggregations
10
- * - Complex sorting and pagination
11
- *
12
- * Run with: bun run test:pglite -- tests/stress/scenarios/realistic-scenarios.test.ts
13
- * Configure: STRESS_ENTITY_COUNT=5000 for larger tests
14
- */
15
- import { describe, test, beforeAll, afterAll, expect } from 'bun:test';
16
- import { DataSeeder } from '../DataSeeder';
17
- import { BenchmarkRunner } from '../BenchmarkRunner';
18
- import { StressTestReporter } from '../StressTestReporter';
19
- import { Query, FilterOp } from '../../../query/Query';
20
- import { OrQuery } from '../../../query/OrQuery';
21
- import {
22
- Product,
23
- Inventory,
24
- Pricing,
25
- Vendor,
26
- ProductMetrics,
27
- CATEGORIES,
28
- SUBCATEGORIES,
29
- REGIONS,
30
- WAREHOUSES,
31
- CURRENCIES,
32
- VENDOR_TIERS,
33
- PRODUCT_STATUSES,
34
- STOCK_STATUSES
35
- } from '../fixtures/RealisticComponents';
36
- import { ensureComponentsRegistered } from '../../utils';
37
-
38
- // Configuration via environment variables
39
- const ENTITY_COUNT = parseInt(process.env.STRESS_ENTITY_COUNT || '1000', 10);
40
- const BATCH_SIZE = Math.min(500, Math.floor(ENTITY_COUNT / 10) || 100);
41
-
42
- // Helper to generate realistic test data
43
- function generateProductData(index: number): Record<string, any> {
44
- const category = CATEGORIES[index % CATEGORIES.length];
45
- const subcategories = SUBCATEGORIES[category];
46
- const subcategory = subcategories[index % subcategories.length];
47
- const status = PRODUCT_STATUSES[index % PRODUCT_STATUSES.length];
48
- const now = new Date();
49
- const createdDaysAgo = Math.floor(Math.random() * 365);
50
-
51
- return {
52
- name: `Product ${index} - ${subcategory}`,
53
- sku: `SKU-${String(index).padStart(6, '0')}`,
54
- description: `This is a ${subcategory.toLowerCase()} product in the ${category} category. High quality item with great reviews.`,
55
- category,
56
- subcategory,
57
- tags: [category.toLowerCase(), subcategory.toLowerCase(), `tag${index % 10}`],
58
- status,
59
- rating: 1 + (Math.random() * 4),
60
- reviewCount: Math.floor(Math.random() * 500),
61
- createdAt: new Date(now.getTime() - createdDaysAgo * 24 * 60 * 60 * 1000),
62
- updatedAt: new Date(now.getTime() - Math.floor(createdDaysAgo / 2) * 24 * 60 * 60 * 1000)
63
- };
64
- }
65
-
66
- function generateInventoryData(index: number): Record<string, any> {
67
- const quantity = Math.floor(Math.random() * 1000);
68
- const reservedQuantity = Math.floor(quantity * Math.random() * 0.3);
69
- const reorderPoint = 10 + Math.floor(Math.random() * 40);
70
-
71
- let stockStatus: string;
72
- if (quantity === 0) stockStatus = 'out_of_stock';
73
- else if (quantity < reorderPoint) stockStatus = 'low_stock';
74
- else if (index % 20 === 0) stockStatus = 'backordered';
75
- else stockStatus = 'in_stock';
76
-
77
- return {
78
- quantity,
79
- reservedQuantity,
80
- warehouseId: WAREHOUSES[index % WAREHOUSES.length],
81
- reorderPoint,
82
- maxStock: 500 + Math.floor(Math.random() * 500),
83
- stockStatus,
84
- lastRestocked: new Date(Date.now() - Math.floor(Math.random() * 30) * 24 * 60 * 60 * 1000)
85
- };
86
- }
87
-
88
- function generatePricingData(index: number): Record<string, any> {
89
- const basePrice = 10 + Math.random() * 990;
90
- const costPrice = basePrice * (0.3 + Math.random() * 0.4);
91
- const isOnSale = index % 5 === 0;
92
- const discountPercent = isOnSale ? 10 + Math.floor(Math.random() * 40) : 0;
93
- const salePrice = isOnSale ? basePrice * (1 - discountPercent / 100) : basePrice;
94
-
95
- return {
96
- basePrice,
97
- salePrice,
98
- costPrice,
99
- currency: CURRENCIES[index % CURRENCIES.length],
100
- discountPercent,
101
- isOnSale,
102
- saleStartDate: isOnSale ? new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) : null,
103
- saleEndDate: isOnSale ? new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) : null,
104
- profit: salePrice - costPrice
105
- };
106
- }
107
-
108
- function generateVendorData(index: number): Record<string, any> {
109
- const vendorIndex = index % 50;
110
- return {
111
- vendorId: `VENDOR-${String(vendorIndex).padStart(3, '0')}`,
112
- vendorName: `Vendor ${vendorIndex} Inc.`,
113
- region: REGIONS[vendorIndex % REGIONS.length],
114
- vendorRating: 3 + Math.random() * 2,
115
- isVerified: vendorIndex % 3 !== 0,
116
- totalSales: Math.floor(Math.random() * 100000),
117
- tier: VENDOR_TIERS[Math.floor(vendorIndex / 12.5)]
118
- };
119
- }
120
-
121
- function generateMetricsData(index: number): Record<string, any> {
122
- const viewCount = Math.floor(Math.random() * 10000);
123
- const cartAddCount = Math.floor(viewCount * (0.1 + Math.random() * 0.2));
124
- const purchaseCount = Math.floor(cartAddCount * (0.2 + Math.random() * 0.3));
125
-
126
- return {
127
- viewCount,
128
- purchaseCount,
129
- cartAddCount,
130
- wishlistCount: Math.floor(Math.random() * 500),
131
- returnCount: Math.floor(purchaseCount * Math.random() * 0.1),
132
- conversionRate: purchaseCount / (viewCount || 1),
133
- lastPurchased: purchaseCount > 0
134
- ? new Date(Date.now() - Math.floor(Math.random() * 30) * 24 * 60 * 60 * 1000)
135
- : null,
136
- popularityScore: viewCount > 5000 ? 'high' : viewCount > 1000 ? 'medium' : 'low'
137
- };
138
- }
139
-
140
- describe('Realistic E-Commerce Stress Tests', () => {
141
- const seeder = new DataSeeder();
142
- const benchmark = new BenchmarkRunner();
143
- const reporter = new StressTestReporter();
144
- let entityIds: string[] = [];
145
- let setupTime = 0;
146
-
147
- beforeAll(async () => {
148
- const startSetup = performance.now();
149
-
150
- console.log(`\n Registering components...`);
151
- await ensureComponentsRegistered(
152
- Product,
153
- Inventory,
154
- Pricing,
155
- Vendor,
156
- ProductMetrics
157
- );
158
-
159
- // Allow index creation to settle
160
- await new Promise(resolve => setTimeout(resolve, 2000));
161
-
162
- console.log(` Seeding ${ENTITY_COUNT.toLocaleString()} product entities...`);
163
-
164
- // Seed primary Product component
165
- const result = await seeder.seed(
166
- Product,
167
- generateProductData,
168
- {
169
- totalEntities: ENTITY_COUNT,
170
- batchSize: BATCH_SIZE,
171
- onProgress: (current, total, elapsed) => {
172
- if (current % (BATCH_SIZE * 5) === 0 || current === total) {
173
- const pct = ((current / total) * 100).toFixed(1);
174
- const rate = ((current / elapsed) * 1000).toFixed(0);
175
- console.log(` Product: ${pct}% (${rate}/sec)`);
176
- }
177
- }
178
- }
179
- );
180
- entityIds = result.entityIds;
181
- console.log(` Products seeded: ${result.recordsPerSecond.toFixed(0)}/sec`);
182
-
183
- // Add Inventory to all products
184
- console.log(` Adding Inventory components...`);
185
- await seeder.seedAdditionalComponent(
186
- entityIds,
187
- Inventory,
188
- generateInventoryData,
189
- BATCH_SIZE
190
- );
191
-
192
- // Add Pricing to all products
193
- console.log(` Adding Pricing components...`);
194
- await seeder.seedAdditionalComponent(
195
- entityIds,
196
- Pricing,
197
- generatePricingData,
198
- BATCH_SIZE
199
- );
200
-
201
- // Add Vendor to 80% of products
202
- const vendorEntityIds = entityIds.slice(0, Math.floor(entityIds.length * 0.8));
203
- console.log(` Adding Vendor components to ${vendorEntityIds.length} products...`);
204
- await seeder.seedAdditionalComponent(
205
- vendorEntityIds,
206
- Vendor,
207
- generateVendorData,
208
- BATCH_SIZE
209
- );
210
-
211
- // Add ProductMetrics to 60% of products
212
- const metricsEntityIds = entityIds.slice(0, Math.floor(entityIds.length * 0.6));
213
- console.log(` Adding ProductMetrics to ${metricsEntityIds.length} products...`);
214
- await seeder.seedAdditionalComponent(
215
- metricsEntityIds,
216
- ProductMetrics,
217
- generateMetricsData,
218
- BATCH_SIZE
219
- );
220
-
221
- console.log(' Running VACUUM ANALYZE...');
222
- await seeder.optimize();
223
-
224
- setupTime = performance.now() - startSetup;
225
- console.log(` Setup complete in ${(setupTime / 1000).toFixed(1)}s\n`);
226
- }, 120000);
227
-
228
- afterAll(async () => {
229
- // Print report
230
- const recordCount = await seeder.getRecordCount();
231
- const report = reporter.generateReport(benchmark.getResults(), {
232
- recordCount,
233
- environment: `PGlite/PostgreSQL, Bun ${Bun.version}`,
234
- duration: setupTime
235
- });
236
- console.log('\n' + report);
237
-
238
- // Cleanup
239
- console.log('\n Cleaning up test data...');
240
- await seeder.cleanup(entityIds, BATCH_SIZE);
241
- console.log(' Cleanup complete.');
242
- }, 60000);
243
-
244
- // ============================================================
245
- // SINGLE COMPONENT FILTER TESTS
246
- // ============================================================
247
-
248
- describe('Single Component Filters', () => {
249
- test('filter by category (indexed)', async () => {
250
- const result = await benchmark.runWithOutput(
251
- 'Product: category=Electronics',
252
- () => new Query()
253
- .with(Product, {
254
- filters: [{ field: 'category', operator: FilterOp.EQ, value: 'Electronics' }]
255
- })
256
- .take(100)
257
- .exec(),
258
- { targetP95: 100, iterations: 10 }
259
- );
260
- expect(result.rowsReturned).toBeGreaterThan(0);
261
- expect(result.passed).toBe(true);
262
- });
263
-
264
- test('filter by status (indexed)', async () => {
265
- const result = await benchmark.runWithOutput(
266
- 'Product: status=active',
267
- () => new Query()
268
- .with(Product, {
269
- filters: [{ field: 'status', operator: FilterOp.EQ, value: 'active' }]
270
- })
271
- .take(100)
272
- .exec(),
273
- { targetP95: 100, iterations: 10 }
274
- );
275
- expect(result.rowsReturned).toBeGreaterThan(0);
276
- });
277
-
278
- test('filter by rating range', async () => {
279
- const result = await benchmark.runWithOutput(
280
- 'Product: rating >= 4',
281
- () => new Query()
282
- .with(Product, {
283
- filters: [{ field: 'rating', operator: FilterOp.GTE, value: 4 }]
284
- })
285
- .take(100)
286
- .exec(),
287
- { targetP95: 100, iterations: 10 }
288
- );
289
- expect(result.rowsReturned).toBeGreaterThan(0);
290
- });
291
-
292
- test('filter by stock status', async () => {
293
- const result = await benchmark.runWithOutput(
294
- 'Inventory: stockStatus=in_stock',
295
- () => new Query()
296
- .with(Inventory, {
297
- filters: [{ field: 'stockStatus', operator: FilterOp.EQ, value: 'in_stock' }]
298
- })
299
- .take(100)
300
- .exec(),
301
- { targetP95: 100, iterations: 10 }
302
- );
303
- expect(result.rowsReturned).toBeGreaterThan(0);
304
- });
305
-
306
- test('filter by price range', async () => {
307
- const result = await benchmark.runWithOutput(
308
- 'Pricing: 50 <= basePrice <= 200',
309
- () => new Query()
310
- .with(Pricing, {
311
- filters: [
312
- { field: 'basePrice', operator: FilterOp.GTE, value: 50 },
313
- { field: 'basePrice', operator: FilterOp.LTE, value: 200 }
314
- ]
315
- })
316
- .take(100)
317
- .exec(),
318
- { targetP95: 100, iterations: 10 }
319
- );
320
- expect(result.rowsReturned).toBeGreaterThan(0);
321
- });
322
-
323
- test('filter by boolean (isOnSale)', async () => {
324
- const result = await benchmark.runWithOutput(
325
- 'Pricing: isOnSale=true',
326
- () => new Query()
327
- .with(Pricing, {
328
- filters: [{ field: 'isOnSale', operator: FilterOp.EQ, value: true }]
329
- })
330
- .take(100)
331
- .exec(),
332
- { targetP95: 100, iterations: 10 }
333
- );
334
- expect(result.rowsReturned).toBeGreaterThan(0);
335
- });
336
- });
337
-
338
- // ============================================================
339
- // MULTI-FILTER SINGLE COMPONENT TESTS
340
- // ============================================================
341
-
342
- describe('Multi-Filter Single Component', () => {
343
- test('active products in Electronics with high rating', async () => {
344
- const result = await benchmark.runWithOutput(
345
- 'Product: active + Electronics + rating>=4',
346
- () => new Query()
347
- .with(Product, {
348
- filters: [
349
- { field: 'status', operator: FilterOp.EQ, value: 'active' },
350
- { field: 'category', operator: FilterOp.EQ, value: 'Electronics' },
351
- { field: 'rating', operator: FilterOp.GTE, value: 4 }
352
- ]
353
- })
354
- .take(50)
355
- .exec(),
356
- { targetP95: 150, iterations: 10 }
357
- );
358
- expect(result.passed).toBe(true);
359
- });
360
-
361
- test('low stock items below reorder point', async () => {
362
- const result = await benchmark.runWithOutput(
363
- 'Inventory: low_stock + qty < 20',
364
- () => new Query()
365
- .with(Inventory, {
366
- filters: [
367
- { field: 'stockStatus', operator: FilterOp.EQ, value: 'low_stock' },
368
- { field: 'quantity', operator: FilterOp.LT, value: 20 }
369
- ]
370
- })
371
- .take(50)
372
- .exec(),
373
- { targetP95: 150, iterations: 10 }
374
- );
375
- expect(result.passed).toBe(true);
376
- });
377
-
378
- test('high margin sale items', async () => {
379
- const result = await benchmark.runWithOutput(
380
- 'Pricing: onSale + profit > 100',
381
- () => new Query()
382
- .with(Pricing, {
383
- filters: [
384
- { field: 'isOnSale', operator: FilterOp.EQ, value: true },
385
- { field: 'profit', operator: FilterOp.GT, value: 100 }
386
- ]
387
- })
388
- .take(50)
389
- .exec(),
390
- { targetP95: 150, iterations: 10 }
391
- );
392
- expect(result.passed).toBe(true);
393
- });
394
-
395
- test('verified gold/platinum vendors with high rating', async () => {
396
- const result = await benchmark.runWithOutput(
397
- 'Vendor: verified + rating>=4 + tier=gold',
398
- () => new Query()
399
- .with(Vendor, {
400
- filters: [
401
- { field: 'isVerified', operator: FilterOp.EQ, value: true },
402
- { field: 'vendorRating', operator: FilterOp.GTE, value: 4 },
403
- { field: 'tier', operator: FilterOp.EQ, value: 'gold' }
404
- ]
405
- })
406
- .take(50)
407
- .exec(),
408
- { targetP95: 150, iterations: 10 }
409
- );
410
- expect(result.passed).toBe(true);
411
- });
412
- });
413
-
414
- // ============================================================
415
- // MULTI-COMPONENT JOIN TESTS
416
- // ============================================================
417
-
418
- describe('Multi-Component Joins', () => {
419
- test('2-way join: Product + Inventory', async () => {
420
- const result = await benchmark.runWithOutput(
421
- 'Join: Product + Inventory',
422
- () => new Query()
423
- .with(Product)
424
- .with(Inventory)
425
- .take(100)
426
- .exec(),
427
- { targetP95: 150, iterations: 10 }
428
- );
429
- expect(result.rowsReturned).toBeGreaterThan(0);
430
- expect(result.passed).toBe(true);
431
- });
432
-
433
- test('3-way join: Product + Inventory + Pricing', async () => {
434
- const result = await benchmark.runWithOutput(
435
- 'Join: Product + Inventory + Pricing',
436
- () => new Query()
437
- .with(Product)
438
- .with(Inventory)
439
- .with(Pricing)
440
- .take(100)
441
- .exec(),
442
- { targetP95: 200, iterations: 10 }
443
- );
444
- expect(result.rowsReturned).toBeGreaterThan(0);
445
- });
446
-
447
- test('4-way join: Product + Inventory + Pricing + Vendor', async () => {
448
- const result = await benchmark.runWithOutput(
449
- 'Join: Product + Inventory + Pricing + Vendor',
450
- () => new Query()
451
- .with(Product)
452
- .with(Inventory)
453
- .with(Pricing)
454
- .with(Vendor)
455
- .take(100)
456
- .exec(),
457
- { targetP95: 300, iterations: 10 }
458
- );
459
- expect(result.rowsReturned).toBeGreaterThan(0);
460
- });
461
-
462
- test('5-way join (all components)', async () => {
463
- const result = await benchmark.runWithOutput(
464
- 'Join: All 5 components',
465
- () => new Query()
466
- .with(Product)
467
- .with(Inventory)
468
- .with(Pricing)
469
- .with(Vendor)
470
- .with(ProductMetrics)
471
- .take(100)
472
- .exec(),
473
- { targetP95: 400, iterations: 10 }
474
- );
475
- expect(result.rowsReturned).toBeGreaterThan(0);
476
- });
477
- });
478
-
479
- // ============================================================
480
- // MULTI-COMPONENT WITH FILTERS TESTS
481
- // ============================================================
482
-
483
- describe('Multi-Component with Filters', () => {
484
- test('active Electronics in stock', async () => {
485
- const result = await benchmark.runWithOutput(
486
- 'Product(active+Electronics) + Inventory(in_stock)',
487
- () => new Query()
488
- .with(Product, {
489
- filters: [
490
- { field: 'status', operator: FilterOp.EQ, value: 'active' },
491
- { field: 'category', operator: FilterOp.EQ, value: 'Electronics' }
492
- ]
493
- })
494
- .with(Inventory, {
495
- filters: [
496
- { field: 'stockStatus', operator: FilterOp.EQ, value: 'in_stock' }
497
- ]
498
- })
499
- .take(50)
500
- .exec(),
501
- { targetP95: 200, iterations: 10 }
502
- );
503
- expect(result.passed).toBe(true);
504
- });
505
-
506
- test('on-sale products with low stock', async () => {
507
- const result = await benchmark.runWithOutput(
508
- 'Pricing(onSale) + Inventory(low_stock)',
509
- () => new Query()
510
- .with(Pricing, {
511
- filters: [{ field: 'isOnSale', operator: FilterOp.EQ, value: true }]
512
- })
513
- .with(Inventory, {
514
- filters: [{ field: 'stockStatus', operator: FilterOp.EQ, value: 'low_stock' }]
515
- })
516
- .take(50)
517
- .exec(),
518
- { targetP95: 200, iterations: 10 }
519
- );
520
- expect(result.passed).toBe(true);
521
- });
522
-
523
- test('high-rated products from verified vendors', async () => {
524
- const result = await benchmark.runWithOutput(
525
- 'Product(rating>=4) + Vendor(verified)',
526
- () => new Query()
527
- .with(Product, {
528
- filters: [{ field: 'rating', operator: FilterOp.GTE, value: 4 }]
529
- })
530
- .with(Vendor, {
531
- filters: [{ field: 'isVerified', operator: FilterOp.EQ, value: true }]
532
- })
533
- .take(50)
534
- .exec(),
535
- { targetP95: 200, iterations: 10 }
536
- );
537
- expect(result.passed).toBe(true);
538
- });
539
-
540
- test('complex: active, in stock, on sale, price range', async () => {
541
- const result = await benchmark.runWithOutput(
542
- 'Complex 4-filter multi-component',
543
- () => new Query()
544
- .with(Product, {
545
- filters: [{ field: 'status', operator: FilterOp.EQ, value: 'active' }]
546
- })
547
- .with(Inventory, {
548
- filters: [{ field: 'stockStatus', operator: FilterOp.EQ, value: 'in_stock' }]
549
- })
550
- .with(Pricing, {
551
- filters: [
552
- { field: 'isOnSale', operator: FilterOp.EQ, value: true },
553
- { field: 'salePrice', operator: FilterOp.LTE, value: 500 }
554
- ]
555
- })
556
- .take(50)
557
- .exec(),
558
- { targetP95: 300, iterations: 10 }
559
- );
560
- expect(result.passed).toBe(true);
561
- });
562
-
563
- test('full pipeline: active + in_stock + onSale + verified + high popularity', async () => {
564
- const result = await benchmark.runWithOutput(
565
- 'Full pipeline: 5-component filtered',
566
- () => new Query()
567
- .with(Product, {
568
- filters: [{ field: 'status', operator: FilterOp.EQ, value: 'active' }]
569
- })
570
- .with(Inventory, {
571
- filters: [{ field: 'stockStatus', operator: FilterOp.EQ, value: 'in_stock' }]
572
- })
573
- .with(Pricing, {
574
- filters: [{ field: 'isOnSale', operator: FilterOp.EQ, value: true }]
575
- })
576
- .with(Vendor, {
577
- filters: [{ field: 'isVerified', operator: FilterOp.EQ, value: true }]
578
- })
579
- .with(ProductMetrics, {
580
- filters: [{ field: 'popularityScore', operator: FilterOp.EQ, value: 'high' }]
581
- })
582
- .take(50)
583
- .exec(),
584
- { targetP95: 500, iterations: 10 }
585
- );
586
- expect(result.passed).toBe(true);
587
- });
588
- });
589
-
590
- // ============================================================
591
- // OR QUERY TESTS
592
- // ============================================================
593
-
594
- describe('OR Queries', () => {
595
- test('OR: active OR pending products', async () => {
596
- const orQuery = new OrQuery([
597
- { component: Product, filters: [{ field: 'status', operator: FilterOp.EQ, value: 'active' }] },
598
- { component: Product, filters: [{ field: 'status', operator: FilterOp.EQ, value: 'pending' }] }
599
- ]);
600
-
601
- const result = await benchmark.runWithOutput(
602
- 'OR: status=active OR status=pending',
603
- () => new Query()
604
- .with(orQuery)
605
- .take(100)
606
- .exec(),
607
- { targetP95: 200, iterations: 10 }
608
- );
609
- expect(result.rowsReturned).toBeGreaterThan(0);
610
- });
611
-
612
- test('OR: low_stock OR out_of_stock', async () => {
613
- const orQuery = new OrQuery([
614
- { component: Inventory, filters: [{ field: 'stockStatus', operator: FilterOp.EQ, value: 'low_stock' }] },
615
- { component: Inventory, filters: [{ field: 'stockStatus', operator: FilterOp.EQ, value: 'out_of_stock' }] }
616
- ]);
617
-
618
- const result = await benchmark.runWithOutput(
619
- 'OR: low_stock OR out_of_stock',
620
- () => new Query()
621
- .with(orQuery)
622
- .take(100)
623
- .exec(),
624
- { targetP95: 200, iterations: 10 }
625
- );
626
- expect(result.rowsReturned).toBeGreaterThan(0);
627
- });
628
-
629
- test('OR: Electronics OR Clothing categories', async () => {
630
- const orQuery = new OrQuery([
631
- { component: Product, filters: [{ field: 'category', operator: FilterOp.EQ, value: 'Electronics' }] },
632
- { component: Product, filters: [{ field: 'category', operator: FilterOp.EQ, value: 'Clothing' }] }
633
- ]);
634
-
635
- const result = await benchmark.runWithOutput(
636
- 'OR: Electronics OR Clothing',
637
- () => new Query()
638
- .with(orQuery)
639
- .take(100)
640
- .exec(),
641
- { targetP95: 200, iterations: 10 }
642
- );
643
- expect(result.rowsReturned).toBeGreaterThan(0);
644
- });
645
- });
646
-
647
- // ============================================================
648
- // EXCLUSION TESTS (without)
649
- // ============================================================
650
-
651
- describe('Component Exclusion (without)', () => {
652
- test('products without Vendor', async () => {
653
- const result = await benchmark.runWithOutput(
654
- 'Product without Vendor',
655
- () => new Query()
656
- .with(Product)
657
- .without(Vendor)
658
- .take(100)
659
- .exec(),
660
- { targetP95: 150, iterations: 10 }
661
- );
662
- // ~20% of products don't have vendors
663
- expect(result.rowsReturned).toBeGreaterThan(0);
664
- });
665
-
666
- test('products without ProductMetrics', async () => {
667
- const result = await benchmark.runWithOutput(
668
- 'Product without Metrics',
669
- () => new Query()
670
- .with(Product)
671
- .without(ProductMetrics)
672
- .take(100)
673
- .exec(),
674
- { targetP95: 150, iterations: 10 }
675
- );
676
- // ~40% of products don't have metrics
677
- expect(result.rowsReturned).toBeGreaterThan(0);
678
- });
679
-
680
- test('in-stock products from non-verified vendors', async () => {
681
- const result = await benchmark.runWithOutput(
682
- 'Inventory(in_stock) + Vendor(!verified)',
683
- () => new Query()
684
- .with(Inventory, {
685
- filters: [{ field: 'stockStatus', operator: FilterOp.EQ, value: 'in_stock' }]
686
- })
687
- .with(Vendor, {
688
- filters: [{ field: 'isVerified', operator: FilterOp.EQ, value: false }]
689
- })
690
- .take(50)
691
- .exec(),
692
- { targetP95: 200, iterations: 10 }
693
- );
694
- expect(result.passed).toBe(true);
695
- });
696
- });
697
-
698
- // ============================================================
699
- // SORTING TESTS
700
- // ============================================================
701
-
702
- describe('Sorting', () => {
703
- test('sort by rating DESC', async () => {
704
- const result = await benchmark.runWithOutput(
705
- 'Product sorted by rating DESC',
706
- () => new Query()
707
- .with(Product)
708
- .sortBy(Product, 'rating', 'DESC')
709
- .take(100)
710
- .exec(),
711
- { targetP95: 150, iterations: 10 }
712
- );
713
- expect(result.rowsReturned).toBeGreaterThan(0);
714
- });
715
-
716
- test('sort by basePrice ASC', async () => {
717
- const result = await benchmark.runWithOutput(
718
- 'Pricing sorted by basePrice ASC',
719
- () => new Query()
720
- .with(Pricing)
721
- .sortBy(Pricing, 'basePrice', 'ASC')
722
- .take(100)
723
- .exec(),
724
- { targetP95: 150, iterations: 10 }
725
- );
726
- expect(result.rowsReturned).toBeGreaterThan(0);
727
- });
728
-
729
- test('filter + sort: active products by rating', async () => {
730
- const result = await benchmark.runWithOutput(
731
- 'Product(active) sorted by rating',
732
- () => new Query()
733
- .with(Product, {
734
- filters: [{ field: 'status', operator: FilterOp.EQ, value: 'active' }]
735
- })
736
- .sortBy(Product, 'rating', 'DESC')
737
- .take(50)
738
- .exec(),
739
- { targetP95: 200, iterations: 10 }
740
- );
741
- expect(result.passed).toBe(true);
742
- });
743
-
744
- test('multi-component filter + sort: on-sale by discount', async () => {
745
- const result = await benchmark.runWithOutput(
746
- 'Pricing(onSale) sorted by discount',
747
- () => new Query()
748
- .with(Product, {
749
- filters: [{ field: 'status', operator: FilterOp.EQ, value: 'active' }]
750
- })
751
- .with(Pricing, {
752
- filters: [{ field: 'isOnSale', operator: FilterOp.EQ, value: true }]
753
- })
754
- .sortBy(Pricing, 'discountPercent', 'DESC')
755
- .take(50)
756
- .exec(),
757
- { targetP95: 250, iterations: 10 }
758
- );
759
- expect(result.passed).toBe(true);
760
- });
761
- });
762
-
763
- // ============================================================
764
- // PAGINATION TESTS
765
- // ============================================================
766
-
767
- describe('Pagination', () => {
768
- test('offset pagination: page 1', async () => {
769
- const result = await benchmark.runWithOutput(
770
- 'Offset: page 1 (0-50)',
771
- () => new Query()
772
- .with(Product)
773
- .take(50)
774
- .offset(0)
775
- .exec(),
776
- { targetP95: 100, iterations: 10 }
777
- );
778
- expect(result.rowsReturned).toBe(50);
779
- });
780
-
781
- test('offset pagination: page 10', async () => {
782
- const result = await benchmark.runWithOutput(
783
- 'Offset: page 10 (450-500)',
784
- () => new Query()
785
- .with(Product)
786
- .take(50)
787
- .offset(450)
788
- .exec(),
789
- { targetP95: 150, iterations: 10 }
790
- );
791
- expect(result.rowsReturned).toBeGreaterThanOrEqual(0);
792
- });
793
-
794
- test('cursor pagination: first page', async () => {
795
- const result = await benchmark.runWithOutput(
796
- 'Cursor: first page',
797
- () => new Query()
798
- .with(Product)
799
- .take(50)
800
- .exec(),
801
- { targetP95: 100, iterations: 10 }
802
- );
803
- expect(result.rowsReturned).toBe(50);
804
- });
805
-
806
- test('cursor pagination: from middle', async () => {
807
- // Get a cursor from the middle
808
- const midpoint = await new Query()
809
- .with(Product)
810
- .take(1)
811
- .offset(Math.floor(ENTITY_COUNT / 2))
812
- .exec();
813
-
814
- const cursorId = midpoint[0]?.id;
815
- if (!cursorId) {
816
- console.log(' Skipping cursor test - no midpoint found');
817
- return;
818
- }
819
-
820
- const result = await benchmark.runWithOutput(
821
- 'Cursor: from middle',
822
- () => new Query()
823
- .with(Product)
824
- .cursor(cursorId)
825
- .take(50)
826
- .exec(),
827
- { targetP95: 100, iterations: 10 }
828
- );
829
- expect(result.passed).toBe(true);
830
- });
831
-
832
- test('filtered pagination: active products page 5', async () => {
833
- const result = await benchmark.runWithOutput(
834
- 'Filtered offset: active page 5',
835
- () => new Query()
836
- .with(Product, {
837
- filters: [{ field: 'status', operator: FilterOp.EQ, value: 'active' }]
838
- })
839
- .take(50)
840
- .offset(200)
841
- .exec(),
842
- { targetP95: 200, iterations: 10 }
843
- );
844
- expect(result.passed).toBe(true);
845
- });
846
- });
847
-
848
- // ============================================================
849
- // AGGREGATION TESTS
850
- // ============================================================
851
-
852
- describe('Aggregations', () => {
853
- test('count all products', async () => {
854
- const result = await benchmark.runWithOutput(
855
- 'COUNT: all products',
856
- async () => [await new Query().with(Product).count()],
857
- { targetP95: 100, iterations: 10 }
858
- );
859
- expect(result.passed).toBe(true);
860
- });
861
-
862
- test('count filtered: active products', async () => {
863
- const result = await benchmark.runWithOutput(
864
- 'COUNT: active products',
865
- async () => [await new Query()
866
- .with(Product, {
867
- filters: [{ field: 'status', operator: FilterOp.EQ, value: 'active' }]
868
- })
869
- .count()],
870
- { targetP95: 100, iterations: 10 }
871
- );
872
- expect(result.passed).toBe(true);
873
- });
874
-
875
- test('count multi-component: active + in_stock', async () => {
876
- const result = await benchmark.runWithOutput(
877
- 'COUNT: active + in_stock',
878
- async () => [await new Query()
879
- .with(Product, {
880
- filters: [{ field: 'status', operator: FilterOp.EQ, value: 'active' }]
881
- })
882
- .with(Inventory, {
883
- filters: [{ field: 'stockStatus', operator: FilterOp.EQ, value: 'in_stock' }]
884
- })
885
- .count()],
886
- { targetP95: 200, iterations: 10 }
887
- );
888
- expect(result.passed).toBe(true);
889
- });
890
- });
891
-
892
- // ============================================================
893
- // POPULATE (EAGER LOADING) TESTS
894
- // ============================================================
895
-
896
- describe('Populate / Eager Loading', () => {
897
- test('populate single component', async () => {
898
- const result = await benchmark.runWithOutput(
899
- 'Populate: Product',
900
- () => new Query()
901
- .with(Product)
902
- .populate()
903
- .take(50)
904
- .exec(),
905
- { targetP95: 150, iterations: 10 }
906
- );
907
- expect(result.rowsReturned).toBe(50);
908
- });
909
-
910
- test('populate multi-component', async () => {
911
- const result = await benchmark.runWithOutput(
912
- 'Populate: Product + Pricing',
913
- () => new Query()
914
- .with(Product)
915
- .with(Pricing)
916
- .populate()
917
- .take(50)
918
- .exec(),
919
- { targetP95: 200, iterations: 10 }
920
- );
921
- expect(result.rowsReturned).toBe(50);
922
- });
923
-
924
- test('filtered populate', async () => {
925
- const result = await benchmark.runWithOutput(
926
- 'Filtered Populate: active + in_stock',
927
- () => new Query()
928
- .with(Product, {
929
- filters: [{ field: 'status', operator: FilterOp.EQ, value: 'active' }]
930
- })
931
- .with(Inventory, {
932
- filters: [{ field: 'stockStatus', operator: FilterOp.EQ, value: 'in_stock' }]
933
- })
934
- .populate()
935
- .take(50)
936
- .exec(),
937
- { targetP95: 250, iterations: 10 }
938
- );
939
- expect(result.passed).toBe(true);
940
- });
941
- });
942
-
943
- // ============================================================
944
- // REAL-WORLD SCENARIO TESTS
945
- // ============================================================
946
-
947
- describe('Real-World Scenarios', () => {
948
- test('homepage featured products: active + in_stock + high rating + sorted', async () => {
949
- const result = await benchmark.runWithOutput(
950
- 'Homepage: featured products',
951
- () => new Query()
952
- .with(Product, {
953
- filters: [
954
- { field: 'status', operator: FilterOp.EQ, value: 'active' },
955
- { field: 'rating', operator: FilterOp.GTE, value: 4 }
956
- ]
957
- })
958
- .with(Inventory, {
959
- filters: [{ field: 'stockStatus', operator: FilterOp.EQ, value: 'in_stock' }]
960
- })
961
- .sortBy(Product, 'rating', 'DESC')
962
- .take(20)
963
- .exec(),
964
- { targetP95: 250, iterations: 10 }
965
- );
966
- expect(result.passed).toBe(true);
967
- });
968
-
969
- test('category page: Electronics with filters and pagination', async () => {
970
- const result = await benchmark.runWithOutput(
971
- 'Category: Electronics page 2',
972
- () => new Query()
973
- .with(Product, {
974
- filters: [
975
- { field: 'category', operator: FilterOp.EQ, value: 'Electronics' },
976
- { field: 'status', operator: FilterOp.EQ, value: 'active' }
977
- ]
978
- })
979
- .with(Inventory, {
980
- filters: [
981
- { field: 'stockStatus', operator: FilterOp.IN, value: ['in_stock', 'low_stock'] }
982
- ]
983
- })
984
- .with(Pricing)
985
- .sortBy(Pricing, 'basePrice', 'ASC')
986
- .take(24)
987
- .offset(24)
988
- .exec(),
989
- { targetP95: 300, iterations: 10 }
990
- );
991
- expect(result.passed).toBe(true);
992
- });
993
-
994
- test('sale page: on-sale products sorted by discount', async () => {
995
- const result = await benchmark.runWithOutput(
996
- 'Sale Page: sorted by discount',
997
- () => new Query()
998
- .with(Product, {
999
- filters: [{ field: 'status', operator: FilterOp.EQ, value: 'active' }]
1000
- })
1001
- .with(Inventory, {
1002
- filters: [{ field: 'stockStatus', operator: FilterOp.EQ, value: 'in_stock' }]
1003
- })
1004
- .with(Pricing, {
1005
- filters: [{ field: 'isOnSale', operator: FilterOp.EQ, value: true }]
1006
- })
1007
- .sortBy(Pricing, 'discountPercent', 'DESC')
1008
- .take(20)
1009
- .exec(),
1010
- { targetP95: 300, iterations: 10 }
1011
- );
1012
- expect(result.passed).toBe(true);
1013
- });
1014
-
1015
- test('admin: low stock alert query', async () => {
1016
- const result = await benchmark.runWithOutput(
1017
- 'Admin: low stock alert',
1018
- () => new Query()
1019
- .with(Product, {
1020
- filters: [{ field: 'status', operator: FilterOp.EQ, value: 'active' }]
1021
- })
1022
- .with(Inventory, {
1023
- filters: [
1024
- { field: 'stockStatus', operator: FilterOp.IN, value: ['low_stock', 'out_of_stock'] }
1025
- ]
1026
- })
1027
- .with(Vendor)
1028
- .populate()
1029
- .take(100)
1030
- .exec(),
1031
- { targetP95: 400, iterations: 10 }
1032
- );
1033
- expect(result.passed).toBe(true);
1034
- });
1035
-
1036
- test('vendor dashboard: products by vendor with metrics', async () => {
1037
- const result = await benchmark.runWithOutput(
1038
- 'Vendor Dashboard: products + metrics',
1039
- () => new Query()
1040
- .with(Product)
1041
- .with(Vendor, {
1042
- filters: [{ field: 'vendorId', operator: FilterOp.EQ, value: 'VENDOR-001' }]
1043
- })
1044
- .with(Inventory)
1045
- .with(ProductMetrics)
1046
- .populate()
1047
- .take(50)
1048
- .exec(),
1049
- { targetP95: 400, iterations: 10 }
1050
- );
1051
- expect(result.passed).toBe(true);
1052
- });
1053
-
1054
- test('search results: price range + category + sorted', async () => {
1055
- const result = await benchmark.runWithOutput(
1056
- 'Search: price 100-500 in Electronics',
1057
- () => new Query()
1058
- .with(Product, {
1059
- filters: [
1060
- { field: 'category', operator: FilterOp.EQ, value: 'Electronics' },
1061
- { field: 'status', operator: FilterOp.EQ, value: 'active' }
1062
- ]
1063
- })
1064
- .with(Pricing, {
1065
- filters: [
1066
- { field: 'basePrice', operator: FilterOp.GTE, value: 100 },
1067
- { field: 'basePrice', operator: FilterOp.LTE, value: 500 }
1068
- ]
1069
- })
1070
- .with(Inventory, {
1071
- filters: [{ field: 'stockStatus', operator: FilterOp.EQ, value: 'in_stock' }]
1072
- })
1073
- .sortBy(Product, 'rating', 'DESC')
1074
- .take(24)
1075
- .exec(),
1076
- { targetP95: 350, iterations: 10 }
1077
- );
1078
- expect(result.passed).toBe(true);
1079
- });
1080
- });
1081
- });