bunsane 0.3.2 → 0.4.0

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