bunsane 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (224) hide show
  1. package/CHANGELOG.md +445 -318
  2. package/config/cache.config.ts +35 -1
  3. package/core/App.ts +24 -1064
  4. package/core/ArcheType.ts +78 -2110
  5. package/core/BatchLoader.ts +56 -32
  6. package/core/Entity.ts +85 -1043
  7. package/core/EntityHookManager.ts +52 -754
  8. package/core/Logger.ts +10 -0
  9. package/core/RequestContext.ts +64 -6
  10. package/core/RequestLoaders.ts +187 -36
  11. package/core/SchedulerManager.ts +28 -600
  12. package/core/app/bootstrap.ts +133 -0
  13. package/core/app/cors.ts +85 -0
  14. package/core/app/graphqlSetup.ts +56 -0
  15. package/core/app/healthEndpoints.ts +31 -0
  16. package/core/app/metricsCollector.ts +27 -0
  17. package/core/app/preparedStatementWarmup.ts +15 -0
  18. package/core/app/processHandlers.ts +43 -0
  19. package/core/app/requestRouter.ts +310 -0
  20. package/core/app/restRegistry.ts +80 -0
  21. package/core/app/shutdown.ts +97 -0
  22. package/core/app/studioRouter.ts +83 -0
  23. package/core/archetype/customTypes.ts +100 -0
  24. package/core/archetype/decorators.ts +171 -0
  25. package/core/archetype/fieldResolvers.ts +666 -0
  26. package/core/archetype/helpers.ts +29 -0
  27. package/core/archetype/relationLoader.ts +161 -0
  28. package/core/archetype/schemaBuilder.ts +141 -0
  29. package/core/archetype/weaver.ts +218 -0
  30. package/core/archetype/zodSchemaBuilder.ts +527 -0
  31. package/core/cache/CacheManager.ts +173 -267
  32. package/core/cache/CompressionUtils.ts +34 -3
  33. package/core/cache/MemoryCache.ts +40 -37
  34. package/core/cache/RedisCache.ts +4 -4
  35. package/core/cache/health.ts +30 -0
  36. package/core/cache/invalidation.ts +96 -0
  37. package/core/cache/strategies/writeInvalidate.ts +111 -0
  38. package/core/cache/strategies/writeThrough.ts +233 -0
  39. package/core/components/BaseComponent.ts +16 -8
  40. package/core/components/ComponentRegistry.ts +28 -0
  41. package/core/decorators/IndexedField.ts +1 -1
  42. package/core/entity/cacheStrategies.ts +97 -0
  43. package/core/entity/componentAccess.ts +364 -0
  44. package/core/entity/finders.ts +202 -0
  45. package/core/entity/pendingOps.ts +72 -0
  46. package/core/entity/saveEntity.ts +377 -0
  47. package/core/hooks/dispatcher.ts +439 -0
  48. package/core/hooks/guards.ts +155 -0
  49. package/core/hooks/registry.ts +247 -0
  50. package/core/metadata/definitions/Component.ts +1 -1
  51. package/core/metadata/index.ts +15 -4
  52. package/core/middleware/AccessLog.ts +8 -1
  53. package/core/middleware/RateLimit.ts +102 -105
  54. package/core/middleware/RequestId.ts +2 -9
  55. package/core/middleware/SecurityHeaders.ts +2 -11
  56. package/core/middleware/headers.ts +28 -0
  57. package/core/remote/OutboxWorker.ts +213 -183
  58. package/core/remote/RemoteManager.ts +401 -400
  59. package/core/remote/types.ts +153 -151
  60. package/core/requestScope.ts +34 -0
  61. package/core/scheduler/cronEvaluator.ts +174 -0
  62. package/core/scheduler/lifecycleHooks.ts +21 -0
  63. package/core/scheduler/lockCoordinator.ts +27 -0
  64. package/core/scheduler/metrics.ts +14 -0
  65. package/core/scheduler/taskRunner.ts +420 -0
  66. package/database/DatabaseHelper.ts +128 -101
  67. package/database/IndexingStrategy.ts +72 -2
  68. package/database/PreparedStatementCache.ts +20 -5
  69. package/database/cancellable.ts +35 -0
  70. package/database/index.ts +15 -3
  71. package/database/instrumentedDb.ts +141 -0
  72. package/endpoints/archetypes.ts +2 -8
  73. package/endpoints/tables.ts +6 -1
  74. package/gql/index.ts +1 -1
  75. package/gql/visitors/ResolverGeneratorVisitor.ts +25 -4
  76. package/package.json +22 -1
  77. package/query/CTENode.ts +5 -3
  78. package/query/ComponentInclusionNode.ts +240 -13
  79. package/query/OrNode.ts +6 -5
  80. package/query/Query.ts +203 -59
  81. package/query/QueryContext.ts +6 -0
  82. package/query/QueryDAG.ts +7 -2
  83. package/query/membershipSource.ts +66 -0
  84. package/storage/LocalStorageProvider.ts +8 -3
  85. package/studio/dist/assets/index-BMZ67Npg.js +254 -0
  86. package/studio/dist/assets/index-BpbuYz9g.css +1 -0
  87. package/studio/{index.html → dist/index.html} +3 -2
  88. package/swagger/generator.ts +11 -1
  89. package/upload/UploadManager.ts +8 -6
  90. package/utils/uuid.ts +40 -10
  91. package/.claude/settings.local.json +0 -47
  92. package/.prettierrc +0 -4
  93. package/.serena/memories/architectural-decision-no-dependency-injection.md +0 -76
  94. package/.serena/memories/architecture.md +0 -154
  95. package/.serena/memories/cache-interface-refactoring-2026-01-24.md +0 -165
  96. package/.serena/memories/code_style_and_conventions.md +0 -76
  97. package/.serena/memories/project_overview.md +0 -43
  98. package/.serena/memories/schema-dsl-plan.md +0 -107
  99. package/.serena/memories/suggested_commands.md +0 -80
  100. package/.serena/memories/typescript-compilation-status.md +0 -54
  101. package/.serena/project.yml +0 -114
  102. package/BunSane.jpg +0 -0
  103. package/CLAUDE.md +0 -198
  104. package/TODO.md +0 -2
  105. package/bun.lock +0 -302
  106. package/bunfig.toml +0 -10
  107. package/docs/SCALABILITY_PLAN.md +0 -175
  108. package/studio/bun.lock +0 -482
  109. package/studio/package.json +0 -39
  110. package/studio/postcss.config.js +0 -6
  111. package/studio/src/components/DataTable.tsx +0 -211
  112. package/studio/src/components/Layout.tsx +0 -13
  113. package/studio/src/components/PageContainer.tsx +0 -9
  114. package/studio/src/components/PageHeader.tsx +0 -13
  115. package/studio/src/components/SearchBar.tsx +0 -57
  116. package/studio/src/components/Sidebar.tsx +0 -294
  117. package/studio/src/components/ui/button.tsx +0 -56
  118. package/studio/src/components/ui/checkbox.tsx +0 -26
  119. package/studio/src/components/ui/input.tsx +0 -25
  120. package/studio/src/hooks/useDataTable.ts +0 -131
  121. package/studio/src/index.css +0 -36
  122. package/studio/src/lib/api.ts +0 -186
  123. package/studio/src/lib/utils.ts +0 -13
  124. package/studio/src/main.tsx +0 -17
  125. package/studio/src/pages/ArcheType.tsx +0 -239
  126. package/studio/src/pages/Components.tsx +0 -124
  127. package/studio/src/pages/EntityInspector.tsx +0 -302
  128. package/studio/src/pages/QueryRunner.tsx +0 -246
  129. package/studio/src/pages/Table.tsx +0 -94
  130. package/studio/src/pages/Welcome.tsx +0 -241
  131. package/studio/src/routes.tsx +0 -45
  132. package/studio/src/store/archeTypeSettings.ts +0 -30
  133. package/studio/src/store/studio.ts +0 -65
  134. package/studio/src/utils/columnHelpers.tsx +0 -114
  135. package/studio/studio-instructions.md +0 -81
  136. package/studio/tailwind.config.js +0 -77
  137. package/studio/utils.ts +0 -54
  138. package/studio/vite.config.js +0 -19
  139. package/tests/benchmark/BENCHMARK_DATABASES_PLAN.md +0 -338
  140. package/tests/benchmark/bunfig.toml +0 -9
  141. package/tests/benchmark/fixtures/EcommerceComponents.ts +0 -283
  142. package/tests/benchmark/fixtures/EcommerceDataGenerators.ts +0 -301
  143. package/tests/benchmark/fixtures/RelationTracker.ts +0 -159
  144. package/tests/benchmark/fixtures/index.ts +0 -6
  145. package/tests/benchmark/index.ts +0 -22
  146. package/tests/benchmark/noop-preload.ts +0 -3
  147. package/tests/benchmark/query-lateral-benchmark.test.ts +0 -372
  148. package/tests/benchmark/runners/BenchmarkLoader.ts +0 -132
  149. package/tests/benchmark/runners/index.ts +0 -4
  150. package/tests/benchmark/scenarios/query-benchmarks.test.ts +0 -465
  151. package/tests/benchmark/scripts/generate-db.ts +0 -344
  152. package/tests/benchmark/scripts/run-benchmarks.ts +0 -97
  153. package/tests/e2e/http.test.ts +0 -130
  154. package/tests/fixtures/archetypes/TestUserArchetype.ts +0 -21
  155. package/tests/fixtures/components/TestOrder.ts +0 -23
  156. package/tests/fixtures/components/TestProduct.ts +0 -23
  157. package/tests/fixtures/components/TestUser.ts +0 -20
  158. package/tests/fixtures/components/index.ts +0 -6
  159. package/tests/graphql/SchemaGeneration.test.ts +0 -90
  160. package/tests/graphql/builders/ResolverBuilder.test.ts +0 -223
  161. package/tests/graphql/builders/TypeDefBuilder.test.ts +0 -153
  162. package/tests/helpers/MockRedisClient.ts +0 -113
  163. package/tests/helpers/MockRedisStreamServer.ts +0 -448
  164. package/tests/integration/archetype/ArcheType.persistence.test.ts +0 -241
  165. package/tests/integration/cache/CacheInvalidation.test.ts +0 -259
  166. package/tests/integration/entity/Entity.persistence.test.ts +0 -333
  167. package/tests/integration/entity/Entity.saveTimeout.test.ts +0 -110
  168. package/tests/integration/query/Query.complexAnalysis.test.ts +0 -557
  169. package/tests/integration/query/Query.edgeCases.test.ts +0 -595
  170. package/tests/integration/query/Query.exec.test.ts +0 -576
  171. package/tests/integration/query/Query.explainAnalyze.test.ts +0 -233
  172. package/tests/integration/query/Query.jsonbArray.test.ts +0 -214
  173. package/tests/integration/remote/dlq.test.ts +0 -175
  174. package/tests/integration/remote/event-dispatch.test.ts +0 -114
  175. package/tests/integration/remote/outbox.test.ts +0 -130
  176. package/tests/integration/remote/rpc.test.ts +0 -177
  177. package/tests/pglite-setup.ts +0 -62
  178. package/tests/setup.ts +0 -164
  179. package/tests/stress/BenchmarkRunner.ts +0 -203
  180. package/tests/stress/DataSeeder.ts +0 -190
  181. package/tests/stress/StressTestReporter.ts +0 -229
  182. package/tests/stress/cursor-perf-test.ts +0 -171
  183. package/tests/stress/fixtures/RealisticComponents.ts +0 -235
  184. package/tests/stress/fixtures/StressTestComponents.ts +0 -58
  185. package/tests/stress/index.ts +0 -7
  186. package/tests/stress/scenarios/query-benchmarks.test.ts +0 -285
  187. package/tests/stress/scenarios/realistic-scenarios.test.ts +0 -1081
  188. package/tests/stress/scenarios/timeout-investigation.test.ts +0 -522
  189. package/tests/unit/BatchLoader.test.ts +0 -196
  190. package/tests/unit/archetype/ArcheType.test.ts +0 -107
  191. package/tests/unit/cache/CacheManager.test.ts +0 -367
  192. package/tests/unit/cache/MemoryCache.test.ts +0 -260
  193. package/tests/unit/cache/RedisCache.test.ts +0 -411
  194. package/tests/unit/entity/Entity.components.test.ts +0 -317
  195. package/tests/unit/entity/Entity.drainSideEffects.test.ts +0 -51
  196. package/tests/unit/entity/Entity.reload.test.ts +0 -63
  197. package/tests/unit/entity/Entity.requireComponents.test.ts +0 -72
  198. package/tests/unit/entity/Entity.test.ts +0 -345
  199. package/tests/unit/gql/depthLimit.test.ts +0 -203
  200. package/tests/unit/gql/operationMiddleware.test.ts +0 -293
  201. package/tests/unit/health/Health.test.ts +0 -129
  202. package/tests/unit/middleware/AccessLog.test.ts +0 -37
  203. package/tests/unit/middleware/Middleware.test.ts +0 -98
  204. package/tests/unit/middleware/RequestId.test.ts +0 -54
  205. package/tests/unit/middleware/SecurityHeaders.test.ts +0 -66
  206. package/tests/unit/query/FilterBuilder.test.ts +0 -111
  207. package/tests/unit/query/JsonbArrayBuilder.test.ts +0 -178
  208. package/tests/unit/query/Query.emptyString.test.ts +0 -69
  209. package/tests/unit/query/Query.test.ts +0 -310
  210. package/tests/unit/remote/CircuitBreaker.test.ts +0 -159
  211. package/tests/unit/remote/RemoteError.test.ts +0 -55
  212. package/tests/unit/remote/decorators.test.ts +0 -195
  213. package/tests/unit/remote/metrics.test.ts +0 -115
  214. package/tests/unit/remote/mockRedisStreamServer.test.ts +0 -104
  215. package/tests/unit/scheduler/DistributedLock.test.ts +0 -274
  216. package/tests/unit/scheduler/SchedulerManager.timeBased.test.ts +0 -95
  217. package/tests/unit/schema/schema-integration.test.ts +0 -426
  218. package/tests/unit/schema/schema.test.ts +0 -580
  219. package/tests/unit/storage/S3StorageProvider.test.ts +0 -567
  220. package/tests/unit/upload/RestUpload.test.ts +0 -267
  221. package/tests/unit/validateEnv.test.ts +0 -82
  222. package/tests/utils/entity-tracker.ts +0 -57
  223. package/tests/utils/index.ts +0 -13
  224. package/tests/utils/test-context.ts +0 -149
@@ -1,293 +0,0 @@
1
- import { describe, it, expect } from "bun:test";
2
- import { GraphQLError } from "graphql";
3
- import {
4
- Middleware,
5
- composeOperationMiddleware,
6
- type OperationMiddleware,
7
- } from "../../../gql/middleware";
8
-
9
- // --- Test helpers ---
10
-
11
- function createMockService() {
12
- return {
13
- getUser: async (args: any, context: any, _info: any) => {
14
- return { id: args.id, name: "Test User", role: context.user?.role };
15
- },
16
- createUser: async (args: any, _context: any, _info: any) => {
17
- return { id: "new-1", name: args.name };
18
- },
19
- };
20
- }
21
-
22
- const Authenticate: OperationMiddleware = async (args, ctx, info, next) => {
23
- if (!ctx.user) {
24
- throw new GraphQLError("Unauthenticated", {
25
- extensions: { code: "UNAUTHENTICATED", http: { status: 401 } },
26
- });
27
- }
28
- return next();
29
- };
30
-
31
- function Authorize(...permissions: string[]): OperationMiddleware {
32
- return async (args, ctx, info, next) => {
33
- const userPerms: string[] = ctx.user?.permissions ?? [];
34
- if (!permissions.every((p) => userPerms.includes(p))) {
35
- throw new GraphQLError("Forbidden", {
36
- extensions: { code: "FORBIDDEN", http: { status: 403 } },
37
- });
38
- }
39
- return next();
40
- };
41
- }
42
-
43
- // --- Tests ---
44
-
45
- describe("composeOperationMiddleware", () => {
46
- it("calls handler directly with empty middleware array", async () => {
47
- const handler = async (args: any) => args.value;
48
- const chain = composeOperationMiddleware([], handler, null);
49
- const result = await chain({ value: 42 }, {}, {});
50
- expect(result).toBe(42);
51
- });
52
-
53
- it("executes middleware in order", async () => {
54
- const order: number[] = [];
55
- const mw1: OperationMiddleware = async (a, c, i, next) => {
56
- order.push(1);
57
- const result = await next();
58
- order.push(4);
59
- return result;
60
- };
61
- const mw2: OperationMiddleware = async (a, c, i, next) => {
62
- order.push(2);
63
- const result = await next();
64
- order.push(3);
65
- return result;
66
- };
67
- const handler = async () => {
68
- order.push(0);
69
- return "done";
70
- };
71
- const chain = composeOperationMiddleware([mw1, mw2], handler, null);
72
- await chain({}, {}, {});
73
- expect(order).toEqual([1, 2, 0, 3, 4]);
74
- });
75
-
76
- it("short-circuits when middleware throws", async () => {
77
- const handlerCalled = { value: false };
78
- const mw: OperationMiddleware = async () => {
79
- throw new GraphQLError("Blocked");
80
- };
81
- const handler = async () => {
82
- handlerCalled.value = true;
83
- };
84
- const chain = composeOperationMiddleware([mw], handler, null);
85
- expect(chain({}, {}, {})).rejects.toThrow("Blocked");
86
- expect(handlerCalled.value).toBe(false);
87
- });
88
-
89
- it("middleware can transform the return value", async () => {
90
- const transform: OperationMiddleware = async (a, c, i, next) => {
91
- const result = await next();
92
- return { ...result, extra: true };
93
- };
94
- const handler = async () => ({ name: "test" });
95
- const chain = composeOperationMiddleware([transform], handler, null);
96
- const result = await chain({}, {}, {});
97
- expect(result).toEqual({ name: "test", extra: true });
98
- });
99
-
100
- it("preserves thisArg for the handler", async () => {
101
- const service = {
102
- prefix: "Hello",
103
- greet: async function (args: any) {
104
- return `${this.prefix} ${args.name}`;
105
- },
106
- };
107
- const passthrough: OperationMiddleware = async (a, c, i, next) => next();
108
- const chain = composeOperationMiddleware(
109
- [passthrough],
110
- service.greet,
111
- service,
112
- );
113
- const result = await chain({ name: "World" }, {}, {});
114
- expect(result).toBe("Hello World");
115
- });
116
- });
117
-
118
- describe("@Middleware decorator", () => {
119
- it("wraps a method with middleware chain", async () => {
120
- const log: string[] = [];
121
- const logger: OperationMiddleware = async (a, c, i, next) => {
122
- log.push("before");
123
- const result = await next();
124
- log.push("after");
125
- return result;
126
- };
127
-
128
- class TestService {
129
- @Middleware([logger])
130
- async getData(args: any, _ctx: any, _info: any) {
131
- log.push("handler");
132
- return { value: args.id };
133
- }
134
- }
135
-
136
- const svc = new TestService();
137
- const result = await svc.getData({ id: 1 }, {}, {});
138
- expect(result).toEqual({ value: 1 });
139
- expect(log).toEqual(["before", "handler", "after"]);
140
- });
141
-
142
- it("chains multiple middleware in order", async () => {
143
- const order: string[] = [];
144
- const first: OperationMiddleware = async (a, c, i, next) => {
145
- order.push("first-in");
146
- const r = await next();
147
- order.push("first-out");
148
- return r;
149
- };
150
- const second: OperationMiddleware = async (a, c, i, next) => {
151
- order.push("second-in");
152
- const r = await next();
153
- order.push("second-out");
154
- return r;
155
- };
156
-
157
- class TestService {
158
- @Middleware([first, second])
159
- async doWork(_args: any, _ctx: any, _info: any) {
160
- order.push("resolver");
161
- return true;
162
- }
163
- }
164
-
165
- await new TestService().doWork({}, {}, {});
166
- expect(order).toEqual([
167
- "first-in",
168
- "second-in",
169
- "resolver",
170
- "second-out",
171
- "first-out",
172
- ]);
173
- });
174
-
175
- it("preserves this context of the service", async () => {
176
- class TestService {
177
- private secret = "s3cret";
178
-
179
- @Middleware([async (a, c, i, next) => next()])
180
- async getSecret(_args: any, _ctx: any, _info: any) {
181
- return this.secret;
182
- }
183
- }
184
-
185
- const result = await new TestService().getSecret({}, {}, {});
186
- expect(result).toBe("s3cret");
187
- });
188
- });
189
-
190
- describe("Auth guard middleware", () => {
191
- it("Authenticate allows requests with user in context", async () => {
192
- const service = createMockService();
193
-
194
- class UserService {
195
- @Middleware([Authenticate])
196
- async getUser(args: any, context: any, info: any) {
197
- return service.getUser(args, context, info);
198
- }
199
- }
200
-
201
- const svc = new UserService();
202
- const result = await svc.getUser(
203
- { id: "1" },
204
- { user: { role: "admin" } },
205
- {},
206
- );
207
- expect(result).toEqual({ id: "1", name: "Test User", role: "admin" });
208
- });
209
-
210
- it("Authenticate rejects requests without user", async () => {
211
- class UserService {
212
- @Middleware([Authenticate])
213
- async getUser(args: any, ctx: any, info: any) {
214
- return { id: args.id };
215
- }
216
- }
217
-
218
- try {
219
- await new UserService().getUser({ id: "1" }, {}, {});
220
- expect.unreachable("Should have thrown");
221
- } catch (err: any) {
222
- expect(err).toBeInstanceOf(GraphQLError);
223
- expect(err.extensions.code).toBe("UNAUTHENTICATED");
224
- expect(err.extensions.http.status).toBe(401);
225
- }
226
- });
227
-
228
- it("Authorize allows requests with correct permissions", async () => {
229
- class UserService {
230
- @Middleware([Authenticate, Authorize("users.read")])
231
- async getUser(args: any, ctx: any, info: any) {
232
- return { id: args.id };
233
- }
234
- }
235
-
236
- const ctx = { user: { permissions: ["users.read", "users.write"] } };
237
- const result = await new UserService().getUser({ id: "1" }, ctx, {});
238
- expect(result).toEqual({ id: "1" });
239
- });
240
-
241
- it("Authorize rejects requests with missing permissions", async () => {
242
- class UserService {
243
- @Middleware([Authenticate, Authorize("users.delete")])
244
- async deleteUser(args: any, ctx: any, info: any) {
245
- return true;
246
- }
247
- }
248
-
249
- const ctx = { user: { permissions: ["users.read"] } };
250
- try {
251
- await new UserService().deleteUser({ id: "1" }, ctx, {});
252
- expect.unreachable("Should have thrown");
253
- } catch (err: any) {
254
- expect(err).toBeInstanceOf(GraphQLError);
255
- expect(err.extensions.code).toBe("FORBIDDEN");
256
- expect(err.extensions.http.status).toBe(403);
257
- }
258
- });
259
-
260
- it("Authenticate runs before Authorize in the chain", async () => {
261
- class UserService {
262
- @Middleware([Authenticate, Authorize("admin")])
263
- async adminOp(_args: any, _ctx: any, _info: any) {
264
- return true;
265
- }
266
- }
267
-
268
- // No user → should get UNAUTHENTICATED, not FORBIDDEN
269
- try {
270
- await new UserService().adminOp({}, {}, {});
271
- expect.unreachable("Should have thrown");
272
- } catch (err: any) {
273
- expect(err.extensions.code).toBe("UNAUTHENTICATED");
274
- }
275
- });
276
-
277
- it("resolver does not execute when middleware rejects", async () => {
278
- let resolverCalled = false;
279
-
280
- class UserService {
281
- @Middleware([Authenticate])
282
- async getUser(_args: any, _ctx: any, _info: any) {
283
- resolverCalled = true;
284
- return {};
285
- }
286
- }
287
-
288
- try {
289
- await new UserService().getUser({}, {}, {});
290
- } catch {}
291
- expect(resolverCalled).toBe(false);
292
- });
293
- });
@@ -1,129 +0,0 @@
1
- import { describe, test, expect, beforeEach } from "bun:test";
2
- import {
3
- deepHealthCheck,
4
- readinessCheck,
5
- type HealthDeps,
6
- } from "../../../core/health";
7
-
8
- let dbUp: boolean;
9
- let cacheUp: boolean;
10
-
11
- function makeDeps(): HealthDeps {
12
- return {
13
- pingDb: async () => {
14
- if (!dbUp) throw new Error("connection refused");
15
- return true;
16
- },
17
- pingCache: async () => cacheUp,
18
- };
19
- }
20
-
21
- describe("deepHealthCheck", () => {
22
- beforeEach(() => {
23
- dbUp = true;
24
- cacheUp = true;
25
- });
26
-
27
- test("returns ok when DB and cache are up", async () => {
28
- const { result, httpStatus } = await deepHealthCheck(makeDeps());
29
-
30
- expect(httpStatus).toBe(200);
31
- expect(result.status).toBe("ok");
32
- expect(result.checks.database.status).toBe("up");
33
- expect(result.checks.cache.status).toBe("up");
34
- expect(typeof result.checks.database.latency_ms).toBe("number");
35
- expect(typeof result.checks.cache.latency_ms).toBe("number");
36
- expect(typeof result.timestamp).toBe("string");
37
- expect(typeof result.uptime).toBe("number");
38
- });
39
-
40
- test("returns degraded when DB is up but cache is down", async () => {
41
- cacheUp = false;
42
-
43
- const { result, httpStatus } = await deepHealthCheck(makeDeps());
44
-
45
- expect(httpStatus).toBe(200);
46
- expect(result.status).toBe("degraded");
47
- expect(result.checks.database.status).toBe("up");
48
- expect(result.checks.cache.status).toBe("down");
49
- });
50
-
51
- test("returns unavailable (503) when DB is down", async () => {
52
- dbUp = false;
53
-
54
- const { result, httpStatus } = await deepHealthCheck(makeDeps());
55
-
56
- expect(httpStatus).toBe(503);
57
- expect(result.status).toBe("unavailable");
58
- expect(result.checks.database.status).toBe("down");
59
- });
60
-
61
- test("returns unavailable (503) when both DB and cache are down", async () => {
62
- dbUp = false;
63
- cacheUp = false;
64
-
65
- const { result, httpStatus } = await deepHealthCheck(makeDeps());
66
-
67
- expect(httpStatus).toBe(503);
68
- expect(result.status).toBe("unavailable");
69
- expect(result.checks.database.status).toBe("down");
70
- expect(result.checks.cache.status).toBe("down");
71
- });
72
- });
73
-
74
- describe("readinessCheck", () => {
75
- beforeEach(() => {
76
- dbUp = true;
77
- cacheUp = true;
78
- });
79
-
80
- test("returns 503 when isReady is false", async () => {
81
- const { result, httpStatus } = await readinessCheck(
82
- false,
83
- false,
84
- makeDeps(),
85
- );
86
-
87
- expect(httpStatus).toBe(503);
88
- expect(result.status).toBe("unavailable");
89
- expect(result.checks.database.status).toBe("unknown");
90
- expect(result.checks.cache.status).toBe("unknown");
91
- });
92
-
93
- test("returns 503 when isShuttingDown is true", async () => {
94
- const { result, httpStatus } = await readinessCheck(
95
- true,
96
- true,
97
- makeDeps(),
98
- );
99
-
100
- expect(httpStatus).toBe(503);
101
- expect(result.status).toBe("unavailable");
102
- });
103
-
104
- test("delegates to deepHealthCheck when ready", async () => {
105
- const { result, httpStatus } = await readinessCheck(
106
- true,
107
- false,
108
- makeDeps(),
109
- );
110
-
111
- expect(httpStatus).toBe(200);
112
- expect(result.status).toBe("ok");
113
- expect(result.checks.database.status).toBe("up");
114
- expect(result.checks.cache.status).toBe("up");
115
- });
116
-
117
- test("returns 503 when ready but DB is down", async () => {
118
- dbUp = false;
119
-
120
- const { result, httpStatus } = await readinessCheck(
121
- true,
122
- false,
123
- makeDeps(),
124
- );
125
-
126
- expect(httpStatus).toBe(503);
127
- expect(result.status).toBe("unavailable");
128
- });
129
- });
@@ -1,37 +0,0 @@
1
- import { describe, test, expect, mock } from 'bun:test';
2
- import { accessLog } from '../../../core/middleware/AccessLog';
3
-
4
- const ok = async () => new Response('ok');
5
-
6
- describe('accessLog middleware', () => {
7
- test('passes through and returns the response', async () => {
8
- const mw = accessLog();
9
- const res = await mw(new Request('http://localhost/test'), ok);
10
-
11
- expect(res.status).toBe(200);
12
- expect(await res.text()).toBe('ok');
13
- });
14
-
15
- test('skips configured paths', async () => {
16
- const mw = accessLog({ skip: ['/health'] });
17
-
18
- // Should still work, just not log
19
- const res = await mw(new Request('http://localhost/health'), ok);
20
- expect(res.status).toBe(200);
21
- });
22
-
23
- test('propagates errors from handler', async () => {
24
- const mw = accessLog();
25
- const failing = async () => { throw new Error('boom'); };
26
-
27
- expect(mw(new Request('http://localhost/'), failing)).rejects.toThrow('boom');
28
- });
29
-
30
- test('handles non-200 responses', async () => {
31
- const mw = accessLog();
32
- const notFound = async () => new Response('not found', { status: 404 });
33
- const res = await mw(new Request('http://localhost/missing'), notFound);
34
-
35
- expect(res.status).toBe(404);
36
- });
37
- });
@@ -1,98 +0,0 @@
1
- import { describe, test, expect } from 'bun:test';
2
- import { composeMiddleware, type Middleware } from '../../../core/Middleware';
3
-
4
- describe('composeMiddleware', () => {
5
- test('calls final handler when no middleware', async () => {
6
- const handler = async (req: Request) => new Response('ok');
7
- const composed = composeMiddleware([], handler);
8
- const res = await composed(new Request('http://localhost/'));
9
- expect(await res.text()).toBe('ok');
10
- });
11
-
12
- test('executes middleware in order (onion model)', async () => {
13
- const order: string[] = [];
14
-
15
- const mw1: Middleware = async (req, next) => {
16
- order.push('mw1-before');
17
- const res = await next();
18
- order.push('mw1-after');
19
- return res;
20
- };
21
-
22
- const mw2: Middleware = async (req, next) => {
23
- order.push('mw2-before');
24
- const res = await next();
25
- order.push('mw2-after');
26
- return res;
27
- };
28
-
29
- const handler = async (req: Request) => {
30
- order.push('handler');
31
- return new Response('ok');
32
- };
33
-
34
- const composed = composeMiddleware([mw1, mw2], handler);
35
- await composed(new Request('http://localhost/'));
36
-
37
- expect(order).toEqual([
38
- 'mw1-before',
39
- 'mw2-before',
40
- 'handler',
41
- 'mw2-after',
42
- 'mw1-after',
43
- ]);
44
- });
45
-
46
- test('middleware can short-circuit (skip next)', async () => {
47
- const mw: Middleware = async (req, next) => {
48
- return new Response('blocked', { status: 403 });
49
- };
50
-
51
- const handler = async (req: Request) => new Response('ok');
52
- const composed = composeMiddleware([mw], handler);
53
- const res = await composed(new Request('http://localhost/'));
54
- expect(res.status).toBe(403);
55
- expect(await res.text()).toBe('blocked');
56
- });
57
-
58
- test('middleware can modify the response', async () => {
59
- const mw: Middleware = async (req, next) => {
60
- const res = await next();
61
- const headers = new Headers(res.headers);
62
- headers.set('X-Custom', 'test');
63
- return new Response(res.body, {
64
- status: res.status,
65
- headers,
66
- });
67
- };
68
-
69
- const handler = async (req: Request) => new Response('ok');
70
- const composed = composeMiddleware([mw], handler);
71
- const res = await composed(new Request('http://localhost/'));
72
- expect(res.headers.get('X-Custom')).toBe('test');
73
- });
74
-
75
- test('errors propagate through middleware chain', async () => {
76
- const mw: Middleware = async (req, next) => {
77
- return next();
78
- };
79
-
80
- const handler = async (req: Request) => {
81
- throw new Error('boom');
82
- };
83
-
84
- const composed = composeMiddleware([mw], handler);
85
- expect(composed(new Request('http://localhost/'))).rejects.toThrow('boom');
86
- });
87
-
88
- test('rejects if next() called multiple times', async () => {
89
- const mw: Middleware = async (req, next) => {
90
- await next();
91
- return next(); // second call should reject
92
- };
93
-
94
- const handler = async (req: Request) => new Response('ok');
95
- const composed = composeMiddleware([mw], handler);
96
- expect(composed(new Request('http://localhost/'))).rejects.toThrow('next() called multiple times');
97
- });
98
- });
@@ -1,54 +0,0 @@
1
- import { describe, test, expect } from 'bun:test';
2
- import { requestId, getRequestId, requestStore } from '../../../core/middleware/RequestId';
3
-
4
- const ok = async () => new Response('ok');
5
-
6
- describe('requestId middleware', () => {
7
- test('generates a UUID and sets X-Request-Id header', async () => {
8
- const mw = requestId();
9
- const res = await mw(new Request('http://localhost/'), ok);
10
-
11
- const id = res.headers.get('X-Request-Id');
12
- expect(id).toBeTruthy();
13
- // UUID format check
14
- expect(id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/);
15
- });
16
-
17
- test('respects incoming X-Request-Id header', async () => {
18
- const mw = requestId();
19
- const req = new Request('http://localhost/', {
20
- headers: { 'X-Request-Id': 'custom-id-123' },
21
- });
22
- const res = await mw(req, ok);
23
-
24
- expect(res.headers.get('X-Request-Id')).toBe('custom-id-123');
25
- });
26
-
27
- test('getRequestId() returns current request ID within middleware', async () => {
28
- const mw = requestId();
29
- let capturedId: string | undefined;
30
-
31
- const handler = async () => {
32
- capturedId = getRequestId();
33
- return new Response('ok');
34
- };
35
-
36
- const res = await mw(new Request('http://localhost/'), handler);
37
- const headerId = res.headers.get('X-Request-Id')!;
38
-
39
- expect(capturedId).toBe(headerId);
40
- });
41
-
42
- test('getRequestId() returns undefined outside request context', () => {
43
- expect(getRequestId()).toBeUndefined();
44
- });
45
-
46
- test('preserves original response body and status', async () => {
47
- const handler = async () => new Response('hello', { status: 201 });
48
- const mw = requestId();
49
- const res = await mw(new Request('http://localhost/'), handler);
50
-
51
- expect(res.status).toBe(201);
52
- expect(await res.text()).toBe('hello');
53
- });
54
- });
@@ -1,66 +0,0 @@
1
- import { describe, test, expect } from 'bun:test';
2
- import { securityHeaders } from '../../../core/middleware/SecurityHeaders';
3
-
4
- const ok = async () => new Response('ok');
5
-
6
- describe('securityHeaders middleware', () => {
7
- test('adds default security headers', async () => {
8
- const mw = securityHeaders();
9
- const res = await mw(new Request('http://localhost/'), ok);
10
-
11
- expect(res.headers.get('X-Frame-Options')).toBe('DENY');
12
- expect(res.headers.get('X-Content-Type-Options')).toBe('nosniff');
13
- expect(res.headers.get('Referrer-Policy')).toBe('strict-origin-when-cross-origin');
14
- });
15
-
16
- test('sets HSTS in production', async () => {
17
- const mw = securityHeaders({ hsts: true });
18
- const res = await mw(new Request('http://localhost/'), ok);
19
-
20
- expect(res.headers.get('Strict-Transport-Security')).toBe('max-age=31536000; includeSubDomains');
21
- });
22
-
23
- test('custom hstsMaxAge', async () => {
24
- const mw = securityHeaders({ hsts: true, hstsMaxAge: 86400 });
25
- const res = await mw(new Request('http://localhost/'), ok);
26
-
27
- expect(res.headers.get('Strict-Transport-Security')).toBe('max-age=86400; includeSubDomains');
28
- });
29
-
30
- test('frameOptions SAMEORIGIN', async () => {
31
- const mw = securityHeaders({ frameOptions: 'SAMEORIGIN' });
32
- const res = await mw(new Request('http://localhost/'), ok);
33
-
34
- expect(res.headers.get('X-Frame-Options')).toBe('SAMEORIGIN');
35
- });
36
-
37
- test('frameOptions disabled', async () => {
38
- const mw = securityHeaders({ frameOptions: false });
39
- const res = await mw(new Request('http://localhost/'), ok);
40
-
41
- expect(res.headers.get('X-Frame-Options')).toBeNull();
42
- });
43
-
44
- test('noSniff disabled', async () => {
45
- const mw = securityHeaders({ noSniff: false });
46
- const res = await mw(new Request('http://localhost/'), ok);
47
-
48
- expect(res.headers.get('X-Content-Type-Options')).toBeNull();
49
- });
50
-
51
- test('referrerPolicy disabled', async () => {
52
- const mw = securityHeaders({ referrerPolicy: false });
53
- const res = await mw(new Request('http://localhost/'), ok);
54
-
55
- expect(res.headers.get('Referrer-Policy')).toBeNull();
56
- });
57
-
58
- test('preserves original response body and status', async () => {
59
- const handler = async () => new Response('hello', { status: 201 });
60
- const mw = securityHeaders();
61
- const res = await mw(new Request('http://localhost/'), handler);
62
-
63
- expect(res.status).toBe(201);
64
- expect(await res.text()).toBe('hello');
65
- });
66
- });