bunsane 0.3.2 → 0.5.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 (220) hide show
  1. package/CHANGELOG.md +471 -370
  2. package/core/BatchLoader.ts +56 -32
  3. package/core/Entity.ts +93 -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 +8 -7
  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 +25 -10
  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 +383 -0
  28. package/core/entity/finders.ts +202 -0
  29. package/core/entity/getCacheManager.ts +10 -0
  30. package/core/entity/pendingOps.ts +72 -0
  31. package/core/entity/saveEntity.ts +375 -0
  32. package/core/health.ts +93 -4
  33. package/core/hooks/dispatcher.ts +439 -0
  34. package/core/hooks/guards.ts +155 -0
  35. package/core/hooks/registry.ts +247 -0
  36. package/core/metadata/definitions/Component.ts +1 -1
  37. package/core/metadata/index.ts +15 -4
  38. package/core/middleware/RateLimit.ts +102 -105
  39. package/core/middleware/RequestId.ts +2 -9
  40. package/core/middleware/SecurityHeaders.ts +2 -11
  41. package/core/middleware/headers.ts +28 -0
  42. package/core/remote/OutboxWorker.ts +213 -183
  43. package/core/remote/RemoteManager.ts +401 -400
  44. package/core/remote/StreamConsumer.ts +535 -535
  45. package/core/remote/types.ts +153 -151
  46. package/core/requestScope.ts +34 -0
  47. package/core/scheduler/cronEvaluator.ts +174 -0
  48. package/core/scheduler/lifecycleHooks.ts +21 -0
  49. package/core/scheduler/lockCoordinator.ts +27 -0
  50. package/core/scheduler/metrics.ts +14 -0
  51. package/core/scheduler/taskRunner.ts +420 -0
  52. package/core/validateEnv.ts +10 -0
  53. package/database/DatabaseHelper.ts +128 -101
  54. package/database/IndexingStrategy.ts +72 -2
  55. package/database/PreparedStatementCache.ts +8 -2
  56. package/database/cancellable.ts +35 -22
  57. package/database/index.ts +29 -3
  58. package/database/instrumentedDb.ts +141 -141
  59. package/database/sqlHelpers.ts +3 -1
  60. package/endpoints/archetypes.ts +2 -8
  61. package/endpoints/tables.ts +6 -1
  62. package/gql/index.ts +1 -1
  63. package/gql/schema/index.ts +15 -4
  64. package/gql/visitors/ResolverGeneratorVisitor.ts +25 -4
  65. package/package.json +22 -1
  66. package/query/CTENode.ts +5 -3
  67. package/query/ComponentInclusionNode.ts +245 -14
  68. package/query/OrNode.ts +8 -19
  69. package/query/Query.ts +208 -79
  70. package/query/QueryContext.ts +6 -0
  71. package/query/QueryDAG.ts +7 -2
  72. package/query/membershipSource.ts +66 -0
  73. package/storage/LocalStorageProvider.ts +8 -3
  74. package/studio/dist/assets/index-BMZ67Npg.js +254 -0
  75. package/studio/dist/assets/index-BpbuYz9g.css +1 -0
  76. package/studio/{index.html → dist/index.html} +3 -2
  77. package/swagger/generator.ts +11 -1
  78. package/upload/UploadManager.ts +8 -6
  79. package/utils/uuid.ts +40 -10
  80. package/.claude/scheduled_tasks.lock +0 -1
  81. package/.claude/settings.local.json +0 -47
  82. package/.prettierrc +0 -4
  83. package/.serena/memories/architectural-decision-no-dependency-injection.md +0 -76
  84. package/.serena/memories/architecture.md +0 -154
  85. package/.serena/memories/cache-interface-refactoring-2026-01-24.md +0 -165
  86. package/.serena/memories/code_style_and_conventions.md +0 -76
  87. package/.serena/memories/project_overview.md +0 -43
  88. package/.serena/memories/schema-dsl-plan.md +0 -107
  89. package/.serena/memories/suggested_commands.md +0 -80
  90. package/.serena/memories/typescript-compilation-status.md +0 -54
  91. package/.serena/project.yml +0 -114
  92. package/BunSane.jpg +0 -0
  93. package/CLAUDE.md +0 -198
  94. package/TODO.md +0 -2
  95. package/bun.lock +0 -302
  96. package/bunfig.toml +0 -10
  97. package/docs/RFC_APP_REFACTOR.md +0 -248
  98. package/docs/RFC_REFACTOR_TARGETS.md +0 -251
  99. package/docs/SCALABILITY_PLAN.md +0 -175
  100. package/studio/bun.lock +0 -482
  101. package/studio/package.json +0 -39
  102. package/studio/postcss.config.js +0 -6
  103. package/studio/src/components/DataTable.tsx +0 -211
  104. package/studio/src/components/Layout.tsx +0 -13
  105. package/studio/src/components/PageContainer.tsx +0 -9
  106. package/studio/src/components/PageHeader.tsx +0 -13
  107. package/studio/src/components/SearchBar.tsx +0 -57
  108. package/studio/src/components/Sidebar.tsx +0 -294
  109. package/studio/src/components/ui/button.tsx +0 -56
  110. package/studio/src/components/ui/checkbox.tsx +0 -26
  111. package/studio/src/components/ui/input.tsx +0 -25
  112. package/studio/src/hooks/useDataTable.ts +0 -131
  113. package/studio/src/index.css +0 -36
  114. package/studio/src/lib/api.ts +0 -186
  115. package/studio/src/lib/utils.ts +0 -13
  116. package/studio/src/main.tsx +0 -17
  117. package/studio/src/pages/ArcheType.tsx +0 -239
  118. package/studio/src/pages/Components.tsx +0 -124
  119. package/studio/src/pages/EntityInspector.tsx +0 -302
  120. package/studio/src/pages/QueryRunner.tsx +0 -246
  121. package/studio/src/pages/Table.tsx +0 -94
  122. package/studio/src/pages/Welcome.tsx +0 -241
  123. package/studio/src/routes.tsx +0 -45
  124. package/studio/src/store/archeTypeSettings.ts +0 -30
  125. package/studio/src/store/studio.ts +0 -65
  126. package/studio/src/utils/columnHelpers.tsx +0 -114
  127. package/studio/studio-instructions.md +0 -81
  128. package/studio/tailwind.config.js +0 -77
  129. package/studio/utils.ts +0 -54
  130. package/studio/vite.config.js +0 -19
  131. package/tests/benchmark/BENCHMARK_DATABASES_PLAN.md +0 -338
  132. package/tests/benchmark/bunfig.toml +0 -9
  133. package/tests/benchmark/fixtures/EcommerceComponents.ts +0 -283
  134. package/tests/benchmark/fixtures/EcommerceDataGenerators.ts +0 -301
  135. package/tests/benchmark/fixtures/RelationTracker.ts +0 -159
  136. package/tests/benchmark/fixtures/index.ts +0 -6
  137. package/tests/benchmark/index.ts +0 -22
  138. package/tests/benchmark/noop-preload.ts +0 -3
  139. package/tests/benchmark/query-lateral-benchmark.test.ts +0 -372
  140. package/tests/benchmark/runners/BenchmarkLoader.ts +0 -132
  141. package/tests/benchmark/runners/index.ts +0 -4
  142. package/tests/benchmark/scenarios/query-benchmarks.test.ts +0 -465
  143. package/tests/benchmark/scripts/generate-db.ts +0 -344
  144. package/tests/benchmark/scripts/run-benchmarks.ts +0 -97
  145. package/tests/e2e/http.test.ts +0 -130
  146. package/tests/fixtures/archetypes/TestUserArchetype.ts +0 -21
  147. package/tests/fixtures/components/TestOrder.ts +0 -23
  148. package/tests/fixtures/components/TestProduct.ts +0 -23
  149. package/tests/fixtures/components/TestUser.ts +0 -20
  150. package/tests/fixtures/components/index.ts +0 -6
  151. package/tests/graphql/SchemaGeneration.test.ts +0 -90
  152. package/tests/graphql/builders/ResolverBuilder.test.ts +0 -223
  153. package/tests/graphql/builders/TypeDefBuilder.test.ts +0 -153
  154. package/tests/helpers/MockRedisClient.ts +0 -113
  155. package/tests/helpers/MockRedisStreamServer.ts +0 -448
  156. package/tests/integration/archetype/ArcheType.persistence.test.ts +0 -241
  157. package/tests/integration/cache/CacheInvalidation.test.ts +0 -259
  158. package/tests/integration/entity/Entity.persistence.test.ts +0 -333
  159. package/tests/integration/entity/Entity.saveTimeout.test.ts +0 -110
  160. package/tests/integration/loaders/RequestLoaders.abort.test.ts +0 -82
  161. package/tests/integration/query/Query.abort.test.ts +0 -66
  162. package/tests/integration/query/Query.complexAnalysis.test.ts +0 -557
  163. package/tests/integration/query/Query.edgeCases.test.ts +0 -595
  164. package/tests/integration/query/Query.exec.test.ts +0 -576
  165. package/tests/integration/query/Query.explainAnalyze.test.ts +0 -233
  166. package/tests/integration/query/Query.jsonbArray.test.ts +0 -214
  167. package/tests/integration/remote/dlq.test.ts +0 -175
  168. package/tests/integration/remote/event-dispatch.test.ts +0 -114
  169. package/tests/integration/remote/outbox.test.ts +0 -130
  170. package/tests/integration/remote/rpc.test.ts +0 -177
  171. package/tests/pglite-setup.ts +0 -62
  172. package/tests/setup.ts +0 -164
  173. package/tests/stress/BenchmarkRunner.ts +0 -203
  174. package/tests/stress/DataSeeder.ts +0 -190
  175. package/tests/stress/StressTestReporter.ts +0 -229
  176. package/tests/stress/cursor-perf-test.ts +0 -171
  177. package/tests/stress/fixtures/RealisticComponents.ts +0 -235
  178. package/tests/stress/fixtures/StressTestComponents.ts +0 -58
  179. package/tests/stress/index.ts +0 -7
  180. package/tests/stress/scenarios/query-benchmarks.test.ts +0 -285
  181. package/tests/stress/scenarios/realistic-scenarios.test.ts +0 -1081
  182. package/tests/stress/scenarios/timeout-investigation.test.ts +0 -522
  183. package/tests/unit/BatchLoader.test.ts +0 -196
  184. package/tests/unit/archetype/ArcheType.test.ts +0 -107
  185. package/tests/unit/cache/CacheManager.test.ts +0 -498
  186. package/tests/unit/cache/MemoryCache.test.ts +0 -260
  187. package/tests/unit/cache/RedisCache.test.ts +0 -411
  188. package/tests/unit/database/cancellable.test.ts +0 -81
  189. package/tests/unit/database/instrumentedDb.test.ts +0 -160
  190. package/tests/unit/entity/Entity.components.test.ts +0 -317
  191. package/tests/unit/entity/Entity.drainSideEffects.test.ts +0 -51
  192. package/tests/unit/entity/Entity.reload.test.ts +0 -63
  193. package/tests/unit/entity/Entity.requireComponents.test.ts +0 -72
  194. package/tests/unit/entity/Entity.test.ts +0 -345
  195. package/tests/unit/gql/depthLimit.test.ts +0 -203
  196. package/tests/unit/gql/operationMiddleware.test.ts +0 -293
  197. package/tests/unit/health/Health.test.ts +0 -129
  198. package/tests/unit/middleware/AccessLog.test.ts +0 -37
  199. package/tests/unit/middleware/Middleware.test.ts +0 -98
  200. package/tests/unit/middleware/RequestId.test.ts +0 -54
  201. package/tests/unit/middleware/SecurityHeaders.test.ts +0 -66
  202. package/tests/unit/query/FilterBuilder.test.ts +0 -111
  203. package/tests/unit/query/JsonbArrayBuilder.test.ts +0 -178
  204. package/tests/unit/query/Query.emptyString.test.ts +0 -69
  205. package/tests/unit/query/Query.test.ts +0 -310
  206. package/tests/unit/remote/CircuitBreaker.test.ts +0 -159
  207. package/tests/unit/remote/RemoteError.test.ts +0 -55
  208. package/tests/unit/remote/decorators.test.ts +0 -195
  209. package/tests/unit/remote/metrics.test.ts +0 -115
  210. package/tests/unit/remote/mockRedisStreamServer.test.ts +0 -104
  211. package/tests/unit/scheduler/DistributedLock.test.ts +0 -274
  212. package/tests/unit/scheduler/SchedulerManager.timeBased.test.ts +0 -95
  213. package/tests/unit/schema/schema-integration.test.ts +0 -426
  214. package/tests/unit/schema/schema.test.ts +0 -580
  215. package/tests/unit/storage/S3StorageProvider.test.ts +0 -567
  216. package/tests/unit/upload/RestUpload.test.ts +0 -267
  217. package/tests/unit/validateEnv.test.ts +0 -82
  218. package/tests/utils/entity-tracker.ts +0 -57
  219. package/tests/utils/index.ts +0 -13
  220. 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
- });