bunsane 0.1.4 → 0.2.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 (257) hide show
  1. package/.claude/settings.local.json +47 -0
  2. package/.claude/skills/update-memory.md +74 -0
  3. package/.prettierrc +4 -0
  4. package/.serena/memories/architectural-decision-no-dependency-injection.md +76 -0
  5. package/.serena/memories/architecture.md +154 -0
  6. package/.serena/memories/cache-interface-refactoring-2026-01-24.md +165 -0
  7. package/.serena/memories/code_style_and_conventions.md +76 -0
  8. package/.serena/memories/project_overview.md +43 -0
  9. package/.serena/memories/schema-dsl-plan.md +107 -0
  10. package/.serena/memories/suggested_commands.md +80 -0
  11. package/.serena/memories/typescript-compilation-status.md +54 -0
  12. package/.serena/project.yml +114 -0
  13. package/TODO.md +1 -7
  14. package/bun.lock +150 -4
  15. package/bunfig.toml +10 -0
  16. package/config/cache.config.ts +77 -0
  17. package/config/upload.config.ts +4 -5
  18. package/core/App.ts +870 -123
  19. package/core/ArcheType.ts +2268 -377
  20. package/core/BatchLoader.ts +181 -71
  21. package/core/Config.ts +153 -0
  22. package/core/Decorators.ts +4 -1
  23. package/core/Entity.ts +621 -92
  24. package/core/EntityHookManager.ts +1 -1
  25. package/core/EntityInterface.ts +3 -1
  26. package/core/EntityManager.ts +1 -13
  27. package/core/ErrorHandler.ts +8 -2
  28. package/core/Logger.ts +9 -0
  29. package/core/Middleware.ts +34 -0
  30. package/core/RequestContext.ts +5 -1
  31. package/core/RequestLoaders.ts +227 -93
  32. package/core/SchedulerManager.ts +193 -52
  33. package/core/cache/CacheAnalytics.ts +399 -0
  34. package/core/cache/CacheFactory.ts +145 -0
  35. package/core/cache/CacheManager.ts +520 -0
  36. package/core/cache/CacheProvider.ts +34 -0
  37. package/core/cache/CacheWarmer.ts +157 -0
  38. package/core/cache/CompressionUtils.ts +110 -0
  39. package/core/cache/MemoryCache.ts +251 -0
  40. package/core/cache/MultiLevelCache.ts +180 -0
  41. package/core/cache/NoOpCache.ts +53 -0
  42. package/core/cache/RedisCache.ts +464 -0
  43. package/core/cache/TTLStrategy.ts +254 -0
  44. package/core/cache/index.ts +6 -0
  45. package/core/components/BaseComponent.ts +120 -0
  46. package/core/{ComponentRegistry.ts → components/ComponentRegistry.ts} +148 -54
  47. package/core/components/Decorators.ts +88 -0
  48. package/core/components/Interfaces.ts +7 -0
  49. package/core/components/index.ts +5 -0
  50. package/core/decorators/EntityHooks.ts +0 -3
  51. package/core/decorators/IndexedField.ts +26 -0
  52. package/core/decorators/ScheduledTask.ts +0 -47
  53. package/core/events/EntityLifecycleEvents.ts +1 -1
  54. package/core/health.ts +112 -0
  55. package/core/metadata/definitions/ArcheType.ts +14 -0
  56. package/core/metadata/definitions/Component.ts +9 -0
  57. package/core/metadata/definitions/gqlObject.ts +1 -1
  58. package/core/metadata/index.ts +42 -1
  59. package/core/metadata/metadata-storage.ts +28 -2
  60. package/core/middleware/AccessLog.ts +59 -0
  61. package/core/middleware/RequestId.ts +38 -0
  62. package/core/middleware/SecurityHeaders.ts +62 -0
  63. package/core/middleware/index.ts +3 -0
  64. package/core/scheduler/DistributedLock.ts +266 -0
  65. package/core/scheduler/index.ts +15 -0
  66. package/core/validateEnv.ts +92 -0
  67. package/database/DatabaseHelper.ts +416 -40
  68. package/database/IndexingStrategy.ts +342 -0
  69. package/database/PreparedStatementCache.ts +226 -0
  70. package/database/index.ts +32 -7
  71. package/database/sqlHelpers.ts +14 -2
  72. package/endpoints/archetypes.ts +362 -0
  73. package/endpoints/components.ts +58 -0
  74. package/endpoints/entity.ts +80 -0
  75. package/endpoints/index.ts +27 -0
  76. package/endpoints/query.ts +93 -0
  77. package/endpoints/stats.ts +76 -0
  78. package/endpoints/tables.ts +212 -0
  79. package/endpoints/types.ts +155 -0
  80. package/gql/ArchetypeOperations.ts +32 -86
  81. package/gql/Generator.ts +27 -315
  82. package/gql/GeneratorV2.ts +37 -0
  83. package/gql/builders/InputTypeBuilder.ts +99 -0
  84. package/gql/builders/ResolverBuilder.ts +234 -0
  85. package/gql/builders/TypeDefBuilder.ts +105 -0
  86. package/gql/builders/index.ts +3 -0
  87. package/gql/decorators/Upload.ts +1 -1
  88. package/gql/depthLimit.ts +85 -0
  89. package/gql/graph/GraphNode.ts +224 -0
  90. package/gql/graph/SchemaGraph.ts +278 -0
  91. package/gql/helpers.ts +8 -2
  92. package/gql/index.ts +56 -4
  93. package/gql/middleware.ts +79 -0
  94. package/gql/orchestration/GraphQLSchemaOrchestrator.ts +241 -0
  95. package/gql/orchestration/index.ts +1 -0
  96. package/gql/scanner/ServiceScanner.ts +347 -0
  97. package/gql/schema/index.ts +458 -0
  98. package/gql/strategies/TypeGenerationStrategy.ts +329 -0
  99. package/gql/types.ts +1 -0
  100. package/gql/utils/TypeSignature.ts +220 -0
  101. package/gql/utils/index.ts +1 -0
  102. package/gql/visitors/ArchetypePreprocessorVisitor.ts +80 -0
  103. package/gql/visitors/DeduplicationVisitor.ts +82 -0
  104. package/gql/visitors/GraphVisitor.ts +78 -0
  105. package/gql/visitors/ResolverGeneratorVisitor.ts +122 -0
  106. package/gql/visitors/SchemaGeneratorVisitor.ts +851 -0
  107. package/gql/visitors/TypeCollectorVisitor.ts +79 -0
  108. package/gql/visitors/VisitorComposer.ts +96 -0
  109. package/gql/visitors/index.ts +7 -0
  110. package/package.json +59 -37
  111. package/plugins/index.ts +2 -2
  112. package/query/CTENode.ts +97 -0
  113. package/query/ComponentInclusionNode.ts +689 -0
  114. package/query/FilterBuilder.ts +127 -0
  115. package/query/FilterBuilderRegistry.ts +202 -0
  116. package/query/OrNode.ts +517 -0
  117. package/query/OrQuery.ts +42 -0
  118. package/query/Query.ts +1022 -0
  119. package/query/QueryContext.ts +170 -0
  120. package/query/QueryDAG.ts +122 -0
  121. package/query/QueryNode.ts +65 -0
  122. package/query/SourceNode.ts +53 -0
  123. package/query/builders/FullTextSearchBuilder.ts +236 -0
  124. package/query/index.ts +21 -0
  125. package/scheduler/index.ts +40 -8
  126. package/service/Service.ts +2 -1
  127. package/service/ServiceRegistry.ts +6 -5
  128. package/{core/storage → storage}/LocalStorageProvider.ts +2 -2
  129. package/storage/S3StorageProvider.ts +316 -0
  130. package/{core/storage → storage}/StorageProvider.ts +7 -3
  131. package/studio/bun.lock +482 -0
  132. package/studio/index.html +13 -0
  133. package/studio/package.json +39 -0
  134. package/studio/postcss.config.js +6 -0
  135. package/studio/src/components/DataTable.tsx +211 -0
  136. package/studio/src/components/Layout.tsx +13 -0
  137. package/studio/src/components/PageContainer.tsx +9 -0
  138. package/studio/src/components/PageHeader.tsx +13 -0
  139. package/studio/src/components/SearchBar.tsx +57 -0
  140. package/studio/src/components/Sidebar.tsx +294 -0
  141. package/studio/src/components/ui/button.tsx +56 -0
  142. package/studio/src/components/ui/checkbox.tsx +26 -0
  143. package/studio/src/components/ui/input.tsx +25 -0
  144. package/studio/src/hooks/useDataTable.ts +131 -0
  145. package/studio/src/index.css +36 -0
  146. package/studio/src/lib/api.ts +186 -0
  147. package/studio/src/lib/utils.ts +13 -0
  148. package/studio/src/main.tsx +17 -0
  149. package/studio/src/pages/ArcheType.tsx +239 -0
  150. package/studio/src/pages/Components.tsx +124 -0
  151. package/studio/src/pages/EntityInspector.tsx +302 -0
  152. package/studio/src/pages/QueryRunner.tsx +246 -0
  153. package/studio/src/pages/Table.tsx +94 -0
  154. package/studio/src/pages/Welcome.tsx +241 -0
  155. package/studio/src/routes.tsx +45 -0
  156. package/studio/src/store/archeTypeSettings.ts +30 -0
  157. package/studio/src/store/studio.ts +65 -0
  158. package/studio/src/utils/columnHelpers.tsx +114 -0
  159. package/studio/studio-instructions.md +81 -0
  160. package/studio/tailwind.config.js +77 -0
  161. package/studio/tsconfig.json +24 -0
  162. package/studio/utils.ts +54 -0
  163. package/studio/vite.config.js +19 -0
  164. package/swagger/generator.ts +1 -1
  165. package/tests/e2e/http.test.ts +126 -0
  166. package/tests/fixtures/archetypes/TestUserArchetype.ts +21 -0
  167. package/tests/fixtures/components/TestOrder.ts +23 -0
  168. package/tests/fixtures/components/TestProduct.ts +23 -0
  169. package/tests/fixtures/components/TestUser.ts +20 -0
  170. package/tests/fixtures/components/index.ts +6 -0
  171. package/tests/graphql/SchemaGeneration.test.ts +90 -0
  172. package/tests/graphql/builders/ResolverBuilder.test.ts +223 -0
  173. package/tests/graphql/builders/TypeDefBuilder.test.ts +153 -0
  174. package/tests/integration/archetype/ArcheType.persistence.test.ts +241 -0
  175. package/tests/integration/cache/CacheInvalidation.test.ts +259 -0
  176. package/tests/integration/entity/Entity.persistence.test.ts +333 -0
  177. package/tests/integration/query/Query.exec.test.ts +523 -0
  178. package/tests/pglite-setup.ts +61 -0
  179. package/tests/setup.ts +164 -0
  180. package/tests/stress/BenchmarkRunner.ts +203 -0
  181. package/tests/stress/DataSeeder.ts +190 -0
  182. package/tests/stress/StressTestReporter.ts +229 -0
  183. package/tests/stress/cursor-perf-test.ts +171 -0
  184. package/tests/stress/fixtures/StressTestComponents.ts +58 -0
  185. package/tests/stress/index.ts +7 -0
  186. package/tests/stress/scenarios/query-benchmarks.test.ts +285 -0
  187. package/tests/unit/BatchLoader.test.ts +82 -0
  188. package/tests/unit/archetype/ArcheType.test.ts +107 -0
  189. package/tests/unit/cache/CacheManager.test.ts +347 -0
  190. package/tests/unit/cache/MemoryCache.test.ts +260 -0
  191. package/tests/unit/cache/RedisCache.test.ts +411 -0
  192. package/tests/unit/entity/Entity.components.test.ts +244 -0
  193. package/tests/unit/entity/Entity.test.ts +345 -0
  194. package/tests/unit/gql/depthLimit.test.ts +203 -0
  195. package/tests/unit/gql/operationMiddleware.test.ts +293 -0
  196. package/tests/unit/health/Health.test.ts +129 -0
  197. package/tests/unit/middleware/AccessLog.test.ts +37 -0
  198. package/tests/unit/middleware/Middleware.test.ts +98 -0
  199. package/tests/unit/middleware/RequestId.test.ts +54 -0
  200. package/tests/unit/middleware/SecurityHeaders.test.ts +66 -0
  201. package/tests/unit/query/FilterBuilder.test.ts +111 -0
  202. package/tests/unit/query/Query.test.ts +308 -0
  203. package/tests/unit/scheduler/DistributedLock.test.ts +274 -0
  204. package/tests/unit/schema/schema-integration.test.ts +426 -0
  205. package/tests/unit/schema/schema.test.ts +580 -0
  206. package/tests/unit/storage/S3StorageProvider.test.ts +571 -0
  207. package/tests/unit/upload/RestUpload.test.ts +267 -0
  208. package/tests/unit/validateEnv.test.ts +82 -0
  209. package/tests/utils/entity-tracker.ts +57 -0
  210. package/tests/utils/index.ts +13 -0
  211. package/tests/utils/test-context.ts +149 -0
  212. package/tsconfig.json +5 -1
  213. package/types/archetype.types.ts +6 -0
  214. package/types/hooks.types.ts +1 -1
  215. package/types/query.types.ts +110 -0
  216. package/types/scheduler.types.ts +68 -7
  217. package/types/upload.types.ts +1 -0
  218. package/{core → upload}/FileValidator.ts +10 -1
  219. package/upload/RestUpload.ts +130 -0
  220. package/{core/components → upload}/UploadComponent.ts +11 -11
  221. package/{core → upload}/UploadManager.ts +3 -3
  222. package/upload/index.ts +23 -7
  223. package/utils/UploadHelper.ts +27 -6
  224. package/utils/cronParser.ts +16 -6
  225. package/.github/workflows/deploy-docs.yml +0 -57
  226. package/core/Components.ts +0 -202
  227. package/core/EntityCache.ts +0 -15
  228. package/core/Query.ts +0 -880
  229. package/docs/README.md +0 -149
  230. package/docs/_coverpage.md +0 -36
  231. package/docs/_sidebar.md +0 -23
  232. package/docs/api/core.md +0 -568
  233. package/docs/api/hooks.md +0 -554
  234. package/docs/api/index.md +0 -222
  235. package/docs/api/query.md +0 -678
  236. package/docs/api/service.md +0 -744
  237. package/docs/core-concepts/archetypes.md +0 -512
  238. package/docs/core-concepts/components.md +0 -498
  239. package/docs/core-concepts/entity.md +0 -314
  240. package/docs/core-concepts/hooks.md +0 -683
  241. package/docs/core-concepts/query.md +0 -588
  242. package/docs/core-concepts/services.md +0 -647
  243. package/docs/examples/code-examples.md +0 -425
  244. package/docs/getting-started.md +0 -337
  245. package/docs/index.html +0 -97
  246. package/tests/bench/insert.bench.ts +0 -60
  247. package/tests/bench/relations.bench.ts +0 -270
  248. package/tests/bench/sorting.bench.ts +0 -416
  249. package/tests/component-hooks-simple.test.ts +0 -117
  250. package/tests/component-hooks.test.ts +0 -1461
  251. package/tests/component.test.ts +0 -339
  252. package/tests/errorHandling.test.ts +0 -155
  253. package/tests/hooks.test.ts +0 -667
  254. package/tests/query-sorting.test.ts +0 -101
  255. package/tests/query.test.ts +0 -81
  256. package/tests/relations.test.ts +0 -170
  257. package/tests/scheduler.test.ts +0 -724
@@ -0,0 +1,285 @@
1
+ /**
2
+ * Stress Tests - Query Performance Benchmarks
3
+ *
4
+ * Tests query performance with configurable data volumes
5
+ * Default: 10,000 records (smoke test)
6
+ * Set STRESS_RECORD_COUNT env var for larger tests
7
+ */
8
+ import { describe, test, beforeAll, afterAll, expect } from 'bun:test';
9
+ import { DataSeeder } from '../DataSeeder';
10
+ import { BenchmarkRunner } from '../BenchmarkRunner';
11
+ import { StressTestReporter } from '../StressTestReporter';
12
+ import { Query, FilterOp } from '../../../query/Query';
13
+ import { StressUser, StressProfile } from '../fixtures/StressTestComponents';
14
+ import { ensureComponentsRegistered } from '../../utils';
15
+
16
+ // Configurable via environment variable
17
+ const RECORD_COUNT = parseInt(process.env.STRESS_RECORD_COUNT || '10000', 10);
18
+ const BATCH_SIZE = Math.min(5000, Math.floor(RECORD_COUNT / 10) || 1000);
19
+
20
+ describe('Stress Tests - Query Performance', () => {
21
+ const seeder = new DataSeeder();
22
+ const benchmark = new BenchmarkRunner();
23
+ const reporter = new StressTestReporter();
24
+ let entityIds: string[] = [];
25
+ let setupTime = 0;
26
+
27
+ beforeAll(async () => {
28
+ const startSetup = performance.now();
29
+
30
+ // Ensure components are registered
31
+ await ensureComponentsRegistered(StressUser, StressProfile);
32
+
33
+ // Wait for index creation to settle (prevents deadlocks from concurrent index creation)
34
+ await new Promise(resolve => setTimeout(resolve, 3000));
35
+
36
+ console.log(`\n Seeding ${RECORD_COUNT.toLocaleString()} records...`);
37
+
38
+ const result = await seeder.seed(
39
+ StressUser,
40
+ (i) => ({
41
+ name: `User ${i}`,
42
+ email: `user${i}@stress.test`,
43
+ age: 18 + (i % 62),
44
+ status: ['active', 'inactive', 'pending', 'banned'][i % 4],
45
+ score: Math.random() * 1000,
46
+ createdAt: new Date(Date.now() - Math.random() * 365 * 24 * 60 * 60 * 1000)
47
+ }),
48
+ {
49
+ totalEntities: RECORD_COUNT,
50
+ batchSize: BATCH_SIZE,
51
+ onProgress: (current, total, elapsed) => {
52
+ if (current % (BATCH_SIZE * 2) === 0 || current === total) {
53
+ const pct = ((current / total) * 100).toFixed(1);
54
+ const rate = ((current / elapsed) * 1000).toFixed(0);
55
+ console.log(` Progress: ${pct}% (${rate} records/sec)`);
56
+ }
57
+ }
58
+ }
59
+ );
60
+
61
+ entityIds = result.entityIds;
62
+ console.log(` Seeded in ${(result.totalTime / 1000).toFixed(1)}s (${result.recordsPerSecond.toFixed(0)} records/sec)`);
63
+
64
+ // Add profile components to 50% of entities
65
+ if (RECORD_COUNT >= 1000) {
66
+ console.log(' Adding profile components to 50% of entities...');
67
+ await seeder.seedAdditionalComponent(
68
+ entityIds.slice(0, Math.floor(entityIds.length / 2)),
69
+ StressProfile,
70
+ (i) => ({
71
+ bio: `This is bio ${i}`,
72
+ avatarUrl: `https://example.com/avatar/${i}.png`,
73
+ verified: i % 3 === 0
74
+ }),
75
+ BATCH_SIZE
76
+ );
77
+ }
78
+
79
+ console.log(' Running VACUUM ANALYZE...');
80
+ await seeder.optimize();
81
+
82
+ setupTime = performance.now() - startSetup;
83
+ console.log(` Setup complete in ${(setupTime / 1000).toFixed(1)}s\n`);
84
+ });
85
+
86
+ afterAll(async () => {
87
+ // Print report
88
+ const recordCount = await seeder.getRecordCount();
89
+ const report = reporter.generateReport(benchmark.getResults(), {
90
+ recordCount,
91
+ environment: `PostgreSQL, Bun ${Bun.version}`,
92
+ duration: setupTime
93
+ });
94
+ console.log('\n' + report);
95
+
96
+ // Cleanup seeded data
97
+ console.log('\n Cleaning up test data...');
98
+ await seeder.cleanup(entityIds, BATCH_SIZE);
99
+ console.log(' Cleanup complete.');
100
+ });
101
+
102
+ test('indexed equality filter (status = active)', async () => {
103
+ const result = await benchmark.runWithOutput(
104
+ 'Filter: status = active',
105
+ () => new Query()
106
+ .with(StressUser, { filters: [{ field: 'status', operator: FilterOp.EQ, value: 'active' }] })
107
+ .take(100)
108
+ .exec(),
109
+ { targetP95: 50, iterations: 15 }
110
+ );
111
+ expect(result.passed).toBe(true);
112
+ });
113
+
114
+ test('indexed range filter (age 25-35)', async () => {
115
+ const result = await benchmark.runWithOutput(
116
+ 'Filter: age 25-35',
117
+ () => new Query()
118
+ .with(StressUser, {
119
+ filters: [
120
+ { field: 'age', operator: FilterOp.GTE, value: 25 },
121
+ { field: 'age', operator: FilterOp.LTE, value: 35 }
122
+ ]
123
+ })
124
+ .take(100)
125
+ .exec(),
126
+ { targetP95: 75, iterations: 15 }
127
+ );
128
+ expect(result.passed).toBe(true);
129
+ });
130
+
131
+ test('count query', async () => {
132
+ const result = await benchmark.runWithOutput(
133
+ 'COUNT all',
134
+ async () => [await new Query().with(StressUser).count()],
135
+ { targetP95: 100, iterations: 15 }
136
+ );
137
+ expect(result.passed).toBe(true);
138
+ });
139
+
140
+ test('pagination - shallow offset (1000)', async () => {
141
+ const result = await benchmark.runWithOutput(
142
+ 'Offset 1000',
143
+ () => new Query()
144
+ .with(StressUser)
145
+ .sortBy(StressUser, 'name', 'ASC')
146
+ .take(100)
147
+ .offset(1000)
148
+ .exec(),
149
+ { targetP95: 75, iterations: 15 }
150
+ );
151
+ expect(result.passed).toBe(true);
152
+ });
153
+
154
+ test('pagination - deep offset (50% of records)', async () => {
155
+ const deepOffset = Math.floor(RECORD_COUNT / 2);
156
+ const result = await benchmark.runWithOutput(
157
+ `Offset ${deepOffset.toLocaleString()}`,
158
+ () => new Query()
159
+ .with(StressUser)
160
+ .sortBy(StressUser, 'name', 'ASC')
161
+ .take(100)
162
+ .offset(deepOffset)
163
+ .exec(),
164
+ { targetP95: 500, iterations: 10 }
165
+ );
166
+ // Deep pagination is expected to be slower, use a more lenient target
167
+ expect(result.timings.p95).toBeLessThan(2000);
168
+ });
169
+
170
+ test('cursor pagination - first page', async () => {
171
+ const result = await benchmark.runWithOutput(
172
+ 'Cursor: first page',
173
+ () => new Query()
174
+ .with(StressUser)
175
+ .take(100)
176
+ .exec(),
177
+ { targetP95: 50, iterations: 15 }
178
+ );
179
+ expect(result.passed).toBe(true);
180
+ });
181
+
182
+ test('cursor pagination - from middle (using cursor)', async () => {
183
+ // First, get an entity ID from the middle of the dataset
184
+ const midpointResults = await new Query()
185
+ .with(StressUser)
186
+ .take(1)
187
+ .offset(Math.floor(RECORD_COUNT / 2))
188
+ .exec();
189
+
190
+ const cursorId = midpointResults[0]?.id;
191
+ expect(cursorId).toBeDefined();
192
+
193
+ const result = await benchmark.runWithOutput(
194
+ 'Cursor: from middle (O(1))',
195
+ () => new Query()
196
+ .with(StressUser)
197
+ .cursor(cursorId!)
198
+ .take(100)
199
+ .exec(),
200
+ { targetP95: 50, iterations: 15 }
201
+ );
202
+ // Cursor pagination should be fast regardless of position
203
+ expect(result.passed).toBe(true);
204
+ });
205
+
206
+ test('cursor pagination - near end (using cursor)', async () => {
207
+ // Get an entity ID from near the end (90%)
208
+ const nearEndResults = await new Query()
209
+ .with(StressUser)
210
+ .take(1)
211
+ .offset(Math.floor(RECORD_COUNT * 0.9))
212
+ .exec();
213
+
214
+ const cursorId = nearEndResults[0]?.id;
215
+ expect(cursorId).toBeDefined();
216
+
217
+ const result = await benchmark.runWithOutput(
218
+ 'Cursor: near end (O(1))',
219
+ () => new Query()
220
+ .with(StressUser)
221
+ .cursor(cursorId!)
222
+ .take(100)
223
+ .exec(),
224
+ { targetP95: 50, iterations: 15 }
225
+ );
226
+ // Cursor pagination should be fast regardless of position
227
+ expect(result.passed).toBe(true);
228
+ });
229
+
230
+ test('multi-component join (User + Profile)', async () => {
231
+ const result = await benchmark.runWithOutput(
232
+ 'Join: User + Profile',
233
+ () => new Query()
234
+ .with(StressUser)
235
+ .with(StressProfile)
236
+ .take(100)
237
+ .exec(),
238
+ { targetP95: 150, iterations: 15 }
239
+ );
240
+ expect(result.passed).toBe(true);
241
+ });
242
+
243
+ test('combined filters and sort', async () => {
244
+ const result = await benchmark.runWithOutput(
245
+ 'Filter + Sort + Limit',
246
+ () => new Query()
247
+ .with(StressUser, {
248
+ filters: [
249
+ { field: 'status', operator: FilterOp.EQ, value: 'active' },
250
+ { field: 'age', operator: FilterOp.GTE, value: 21 }
251
+ ]
252
+ })
253
+ .sortBy(StressUser, 'score', 'DESC')
254
+ .take(50)
255
+ .exec(),
256
+ { targetP95: 100, iterations: 15 }
257
+ );
258
+ expect(result.passed).toBe(true);
259
+ });
260
+
261
+ test('simple query without filters', async () => {
262
+ const result = await benchmark.runWithOutput(
263
+ 'Simple: take 100',
264
+ () => new Query()
265
+ .with(StressUser)
266
+ .take(100)
267
+ .exec(),
268
+ { targetP95: 50, iterations: 15 }
269
+ );
270
+ expect(result.passed).toBe(true);
271
+ });
272
+
273
+ test('query with populate', async () => {
274
+ const result = await benchmark.runWithOutput(
275
+ 'Populated: take 50',
276
+ () => new Query()
277
+ .with(StressUser)
278
+ .populate()
279
+ .take(50)
280
+ .exec(),
281
+ { targetP95: 100, iterations: 15 }
282
+ );
283
+ expect(result.passed).toBe(true);
284
+ });
285
+ });
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Unit tests for BatchLoader TTL behavior
3
+ */
4
+ import { describe, test, expect, beforeEach } from 'bun:test';
5
+ import { BatchLoader } from '../../core/BatchLoader';
6
+
7
+ describe('BatchLoader', () => {
8
+ beforeEach(() => {
9
+ BatchLoader.clearCache();
10
+ });
11
+
12
+ describe('getCacheStats()', () => {
13
+ test('returns zero stats when cache is empty', () => {
14
+ const stats = BatchLoader.getCacheStats();
15
+ expect(stats.size).toBe(0);
16
+ expect(stats.entries).toBe(0);
17
+ expect(stats.expired).toBe(0);
18
+ });
19
+ });
20
+
21
+ describe('clearCache()', () => {
22
+ test('clears all cached entries', () => {
23
+ // Access internal cache via any to set up test data
24
+ const cache = (BatchLoader as any).cache as Map<string, Map<string, any>>;
25
+ const innerMap = new Map();
26
+ innerMap.set('parent1', { ids: ['a', 'b'], expiresAt: Date.now() + 300000 });
27
+ cache.set('type1:value', innerMap);
28
+
29
+ expect(BatchLoader.getCacheStats().entries).toBe(1);
30
+
31
+ BatchLoader.clearCache();
32
+ expect(BatchLoader.getCacheStats().entries).toBe(0);
33
+ });
34
+ });
35
+
36
+ describe('TTL expiry', () => {
37
+ test('getCacheStats reports expired entries', () => {
38
+ const cache = (BatchLoader as any).cache as Map<string, Map<string, any>>;
39
+ const innerMap = new Map();
40
+ // One fresh entry
41
+ innerMap.set('parent1', { ids: ['a'], expiresAt: Date.now() + 300000 });
42
+ // One expired entry
43
+ innerMap.set('parent2', { ids: ['b'], expiresAt: Date.now() - 1000 });
44
+ cache.set('type1:value', innerMap);
45
+
46
+ const stats = BatchLoader.getCacheStats();
47
+ expect(stats.entries).toBe(2);
48
+ expect(stats.expired).toBe(1);
49
+ });
50
+
51
+ test('expired entries are counted correctly for multiple type keys', () => {
52
+ const cache = (BatchLoader as any).cache as Map<string, Map<string, any>>;
53
+
54
+ const map1 = new Map();
55
+ map1.set('p1', { ids: ['a'], expiresAt: Date.now() - 5000 });
56
+ map1.set('p2', { ids: ['b'], expiresAt: Date.now() + 300000 });
57
+ cache.set('type1:field', map1);
58
+
59
+ const map2 = new Map();
60
+ map2.set('p3', { ids: ['c'], expiresAt: Date.now() - 1000 });
61
+ cache.set('type2:field', map2);
62
+
63
+ const stats = BatchLoader.getCacheStats();
64
+ expect(stats.size).toBe(2);
65
+ expect(stats.entries).toBe(3);
66
+ expect(stats.expired).toBe(2);
67
+ });
68
+
69
+ test('all entries fresh means zero expired', () => {
70
+ const cache = (BatchLoader as any).cache as Map<string, Map<string, any>>;
71
+ const innerMap = new Map();
72
+ innerMap.set('p1', { ids: ['a'], expiresAt: Date.now() + 60000 });
73
+ innerMap.set('p2', { ids: ['b'], expiresAt: Date.now() + 60000 });
74
+ innerMap.set('p3', { ids: ['c'], expiresAt: Date.now() + 60000 });
75
+ cache.set('type:field', innerMap);
76
+
77
+ const stats = BatchLoader.getCacheStats();
78
+ expect(stats.entries).toBe(3);
79
+ expect(stats.expired).toBe(0);
80
+ });
81
+ });
82
+ });
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Unit tests for ArcheType system
3
+ * Tests archetype definition and basic functionality
4
+ */
5
+ import { describe, test, expect, beforeAll } from 'bun:test';
6
+ import { BaseArcheType, ArcheType, ArcheTypeField } from '../../../core/ArcheType';
7
+ import { TestUser, TestProduct, TestOrder } from '../../fixtures/components';
8
+ import { TestUserArchetype, TestUserWithOrdersArchetype } from '../../fixtures/archetypes/TestUserArchetype';
9
+ import { ensureComponentsRegistered } from '../../utils';
10
+
11
+ describe('ArcheType', () => {
12
+ beforeAll(async () => {
13
+ await ensureComponentsRegistered(TestUser, TestProduct, TestOrder);
14
+ });
15
+
16
+ describe('ArcheType definition', () => {
17
+ test('archetype class extends BaseArcheType', () => {
18
+ const archetype = new TestUserArchetype();
19
+ expect(archetype).toBeInstanceOf(BaseArcheType);
20
+ });
21
+
22
+ test('archetype has componentMap', () => {
23
+ const archetype = new TestUserArchetype();
24
+ expect(archetype.componentMap).toBeDefined();
25
+ expect(typeof archetype.componentMap).toBe('object');
26
+ });
27
+
28
+ test('componentMap contains declared fields', () => {
29
+ const archetype = new TestUserArchetype();
30
+ expect(archetype.componentMap.user).toBeDefined();
31
+ });
32
+
33
+ test('archetype with multiple components', () => {
34
+ const archetype = new TestUserWithOrdersArchetype();
35
+ expect(archetype.componentMap.user).toBeDefined();
36
+ expect(archetype.componentMap.order).toBeDefined();
37
+ });
38
+ });
39
+
40
+ describe('createEntity()', () => {
41
+ test('creates entity with id', () => {
42
+ const archetype = new TestUserArchetype();
43
+ const entity = archetype.createEntity();
44
+
45
+ expect(entity).toBeDefined();
46
+ expect(entity.id).toBeDefined();
47
+ expect(entity.id.length).toBeGreaterThan(0);
48
+ });
49
+
50
+ test('entity is dirty after creation', () => {
51
+ const archetype = new TestUserArchetype();
52
+ const entity = archetype.createEntity();
53
+
54
+ expect((entity as any)._dirty).toBe(true);
55
+ });
56
+
57
+ test('entity is not persisted after creation', () => {
58
+ const archetype = new TestUserArchetype();
59
+ const entity = archetype.createEntity();
60
+
61
+ expect(entity._persisted).toBe(false);
62
+ });
63
+ });
64
+
65
+ describe('getZodObjectSchema()', () => {
66
+ test('returns zod schema for archetype', () => {
67
+ const archetype = new TestUserArchetype();
68
+ const schema = archetype.getZodObjectSchema();
69
+
70
+ expect(schema).toBeDefined();
71
+ });
72
+ });
73
+
74
+ describe('getInputSchema()', () => {
75
+ test('returns input schema for archetype', () => {
76
+ const archetype = new TestUserArchetype();
77
+ const schema = archetype.getInputSchema();
78
+
79
+ expect(schema).toBeDefined();
80
+ });
81
+ });
82
+
83
+ describe('getComponentsToLoad()', () => {
84
+ test('returns non-empty components array', () => {
85
+ const archetype = new TestUserArchetype();
86
+ const components = (archetype as any).getComponentsToLoad();
87
+
88
+ expect(Array.isArray(components)).toBe(true);
89
+ expect(components.length).toBeGreaterThan(0);
90
+ expect(components).toContain(TestUser);
91
+ });
92
+ });
93
+
94
+ describe('withValidation()', () => {
95
+ test('returns a Zod schema with validations applied', () => {
96
+ const archetype = new TestUserArchetype();
97
+ const schema = archetype.withValidation({
98
+ user: { name: 'Valid', email: 'valid@test.com', age: 25 }
99
+ });
100
+
101
+ expect(schema).toBeDefined();
102
+ // Should return a Zod schema with a shape property
103
+ expect(schema.shape).toBeDefined();
104
+ expect(typeof schema.safeParse).toBe('function');
105
+ });
106
+ });
107
+ });