@fragno-dev/core 0.1.7 → 0.1.9

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 (183) hide show
  1. package/.turbo/turbo-build.log +131 -64
  2. package/CHANGELOG.md +19 -0
  3. package/dist/api/api.d.ts +38 -2
  4. package/dist/api/api.d.ts.map +1 -0
  5. package/dist/api/api.js +9 -2
  6. package/dist/api/api.js.map +1 -0
  7. package/dist/api/bind-services.d.ts +6 -0
  8. package/dist/api/bind-services.d.ts.map +1 -0
  9. package/dist/api/bind-services.js +20 -0
  10. package/dist/api/bind-services.js.map +1 -0
  11. package/dist/api/error.d.ts +26 -0
  12. package/dist/api/error.d.ts.map +1 -0
  13. package/dist/{api-DngJDcmO.js → api/error.js} +2 -8
  14. package/dist/api/error.js.map +1 -0
  15. package/dist/api/fragment-definition-builder.d.ts +313 -0
  16. package/dist/api/fragment-definition-builder.d.ts.map +1 -0
  17. package/dist/api/fragment-definition-builder.js +326 -0
  18. package/dist/api/fragment-definition-builder.js.map +1 -0
  19. package/dist/api/fragment-instantiator.d.ts +216 -0
  20. package/dist/api/fragment-instantiator.d.ts.map +1 -0
  21. package/dist/api/fragment-instantiator.js +487 -0
  22. package/dist/api/fragment-instantiator.js.map +1 -0
  23. package/dist/api/fragno-response.d.ts +30 -0
  24. package/dist/api/fragno-response.d.ts.map +1 -0
  25. package/dist/api/fragno-response.js +73 -0
  26. package/dist/api/fragno-response.js.map +1 -0
  27. package/dist/api/internal/path.d.ts +50 -0
  28. package/dist/api/internal/path.d.ts.map +1 -0
  29. package/dist/api/internal/path.js +76 -0
  30. package/dist/api/internal/path.js.map +1 -0
  31. package/dist/api/internal/response-stream.d.ts +43 -0
  32. package/dist/api/internal/response-stream.d.ts.map +1 -0
  33. package/dist/api/internal/response-stream.js +81 -0
  34. package/dist/api/internal/response-stream.js.map +1 -0
  35. package/dist/api/internal/route.js +10 -0
  36. package/dist/api/internal/route.js.map +1 -0
  37. package/dist/api/mutable-request-state.d.ts +82 -0
  38. package/dist/api/mutable-request-state.d.ts.map +1 -0
  39. package/dist/api/mutable-request-state.js +97 -0
  40. package/dist/api/mutable-request-state.js.map +1 -0
  41. package/dist/api/request-context-storage.d.ts +42 -0
  42. package/dist/api/request-context-storage.d.ts.map +1 -0
  43. package/dist/api/request-context-storage.js +43 -0
  44. package/dist/api/request-context-storage.js.map +1 -0
  45. package/dist/api/request-input-context.d.ts +89 -0
  46. package/dist/api/request-input-context.d.ts.map +1 -0
  47. package/dist/api/request-input-context.js +118 -0
  48. package/dist/api/request-input-context.js.map +1 -0
  49. package/dist/api/request-middleware.d.ts +50 -0
  50. package/dist/api/request-middleware.d.ts.map +1 -0
  51. package/dist/api/request-middleware.js +83 -0
  52. package/dist/api/request-middleware.js.map +1 -0
  53. package/dist/api/request-output-context.d.ts +41 -0
  54. package/dist/api/request-output-context.d.ts.map +1 -0
  55. package/dist/api/request-output-context.js +119 -0
  56. package/dist/api/request-output-context.js.map +1 -0
  57. package/dist/api/route-handler-input-options.d.ts +21 -0
  58. package/dist/api/route-handler-input-options.d.ts.map +1 -0
  59. package/dist/api/route.d.ts +54 -3
  60. package/dist/api/route.d.ts.map +1 -0
  61. package/dist/api/route.js +29 -2
  62. package/dist/api/route.js.map +1 -0
  63. package/dist/api/shared-types.d.ts +47 -0
  64. package/dist/api/shared-types.d.ts.map +1 -0
  65. package/dist/api/shared-types.js +1 -0
  66. package/dist/client/client-error.d.ts +60 -0
  67. package/dist/client/client-error.d.ts.map +1 -0
  68. package/dist/client/client-error.js +92 -0
  69. package/dist/client/client-error.js.map +1 -0
  70. package/dist/client/client.d.ts +210 -4
  71. package/dist/client/client.d.ts.map +1 -0
  72. package/dist/client/client.js +397 -6
  73. package/dist/client/client.js.map +1 -0
  74. package/dist/client/client.svelte.d.ts +5 -3
  75. package/dist/client/client.svelte.d.ts.map +1 -1
  76. package/dist/client/client.svelte.js +1 -5
  77. package/dist/client/client.svelte.js.map +1 -1
  78. package/dist/client/internal/fetcher-merge.js +36 -0
  79. package/dist/client/internal/fetcher-merge.js.map +1 -0
  80. package/dist/client/internal/ndjson-streaming.js +139 -0
  81. package/dist/client/internal/ndjson-streaming.js.map +1 -0
  82. package/dist/client/react.d.ts +5 -3
  83. package/dist/client/react.d.ts.map +1 -1
  84. package/dist/client/react.js +3 -5
  85. package/dist/client/react.js.map +1 -1
  86. package/dist/client/solid.d.ts +5 -3
  87. package/dist/client/solid.d.ts.map +1 -1
  88. package/dist/client/solid.js +2 -5
  89. package/dist/client/solid.js.map +1 -1
  90. package/dist/client/vanilla.d.ts +5 -3
  91. package/dist/client/vanilla.d.ts.map +1 -1
  92. package/dist/client/vanilla.js +2 -43
  93. package/dist/client/vanilla.js.map +1 -1
  94. package/dist/client/vue.d.ts +5 -3
  95. package/dist/client/vue.d.ts.map +1 -1
  96. package/dist/client/vue.js +1 -5
  97. package/dist/client/vue.js.map +1 -1
  98. package/dist/http/http-status.d.ts +26 -0
  99. package/dist/http/http-status.d.ts.map +1 -0
  100. package/dist/integrations/react-ssr.js +1 -1
  101. package/dist/internal/symbols.d.ts +9 -0
  102. package/dist/internal/symbols.d.ts.map +1 -0
  103. package/dist/internal/symbols.js +10 -0
  104. package/dist/internal/symbols.js.map +1 -0
  105. package/dist/mod-client.d.ts +36 -0
  106. package/dist/mod-client.d.ts.map +1 -0
  107. package/dist/mod-client.js +21 -0
  108. package/dist/mod-client.js.map +1 -0
  109. package/dist/mod.d.ts +7 -4
  110. package/dist/mod.js +4 -6
  111. package/dist/request/request.d.ts +4 -0
  112. package/dist/request/request.js +5 -0
  113. package/dist/test/test.d.ts +62 -35
  114. package/dist/test/test.d.ts.map +1 -1
  115. package/dist/test/test.js +75 -40
  116. package/dist/test/test.js.map +1 -1
  117. package/dist/util/async.js +40 -0
  118. package/dist/util/async.js.map +1 -0
  119. package/dist/util/content-type.js +49 -0
  120. package/dist/util/content-type.js.map +1 -0
  121. package/dist/util/nanostores.js +31 -0
  122. package/dist/util/nanostores.js.map +1 -0
  123. package/dist/{ssr-BByDVfFD.js → util/ssr.js} +2 -2
  124. package/dist/util/ssr.js.map +1 -0
  125. package/dist/util/types-util.d.ts +8 -0
  126. package/dist/util/types-util.d.ts.map +1 -0
  127. package/package.json +19 -12
  128. package/src/api/api.ts +41 -6
  129. package/src/api/bind-services.ts +42 -0
  130. package/src/api/fragment-definition-builder.extend.test.ts +810 -0
  131. package/src/api/fragment-definition-builder.test.ts +499 -0
  132. package/src/api/fragment-definition-builder.ts +1088 -0
  133. package/src/api/fragment-instantiator.test.ts +1488 -0
  134. package/src/api/fragment-instantiator.ts +1053 -0
  135. package/src/api/fragment-services.test.ts +727 -0
  136. package/src/api/request-context-storage.ts +64 -0
  137. package/src/api/request-middleware.test.ts +301 -225
  138. package/src/api/route.test.ts +87 -1
  139. package/src/api/route.ts +345 -24
  140. package/src/api/shared-types.ts +43 -0
  141. package/src/client/client-builder.test.ts +23 -23
  142. package/src/client/client.ssr.test.ts +3 -3
  143. package/src/client/client.svelte.test.ts +15 -15
  144. package/src/client/client.test.ts +22 -22
  145. package/src/client/client.ts +72 -12
  146. package/src/client/internal/fetcher-merge.ts +1 -1
  147. package/src/client/react.test.ts +2 -2
  148. package/src/client/solid.test.ts +2 -2
  149. package/src/client/vanilla.test.ts +2 -2
  150. package/src/client/vue.test.ts +2 -2
  151. package/src/internal/symbols.ts +5 -0
  152. package/src/mod-client.ts +59 -0
  153. package/src/mod.ts +26 -9
  154. package/src/request/request.ts +8 -0
  155. package/src/test/test.test.ts +200 -381
  156. package/src/test/test.ts +190 -117
  157. package/tsdown.config.ts +8 -5
  158. package/dist/api/fragment-builder.d.ts +0 -4
  159. package/dist/api/fragment-builder.js +0 -3
  160. package/dist/api/fragment-instantiation.d.ts +0 -4
  161. package/dist/api/fragment-instantiation.js +0 -6
  162. package/dist/api-BWN97TOr.d.ts +0 -377
  163. package/dist/api-BWN97TOr.d.ts.map +0 -1
  164. package/dist/api-DngJDcmO.js.map +0 -1
  165. package/dist/client-C5LsYHEI.js +0 -782
  166. package/dist/client-C5LsYHEI.js.map +0 -1
  167. package/dist/fragment-builder-DOnCVBqc.js +0 -47
  168. package/dist/fragment-builder-DOnCVBqc.js.map +0 -1
  169. package/dist/fragment-builder-MGr68GNb.d.ts +0 -409
  170. package/dist/fragment-builder-MGr68GNb.d.ts.map +0 -1
  171. package/dist/fragment-instantiation-C4wvwl6V.js +0 -446
  172. package/dist/fragment-instantiation-C4wvwl6V.js.map +0 -1
  173. package/dist/request-output-context-CdIjwmEN.js +0 -320
  174. package/dist/request-output-context-CdIjwmEN.js.map +0 -1
  175. package/dist/route-Bl9Zr1Yv.d.ts +0 -26
  176. package/dist/route-Bl9Zr1Yv.d.ts.map +0 -1
  177. package/dist/route-C5Uryylh.js +0 -21
  178. package/dist/route-C5Uryylh.js.map +0 -1
  179. package/dist/ssr-BByDVfFD.js.map +0 -1
  180. package/src/api/fragment-builder.ts +0 -80
  181. package/src/api/fragment-instantiation.test.ts +0 -460
  182. package/src/api/fragment-instantiation.ts +0 -499
  183. package/src/api/fragment.test.ts +0 -537
@@ -1,449 +1,268 @@
1
- import { describe, it, expect, expectTypeOf } from "vitest";
2
- import { createFragmentForTest } from "./test";
3
- import { defineFragment } from "../api/fragment-builder";
4
- import { defineRoute, defineRoutes } from "../api/route";
1
+ import { describe, it, expect } from "vitest";
2
+ import { createFragmentForTest, withTestUtils } from "./test";
3
+ import { defineFragment } from "../api/fragment-definition-builder";
4
+ import { defineRoutes } from "../api/route";
5
5
  import { z } from "zod";
6
6
 
7
- describe("createFragmentForTest", () => {
8
- it("should create a test fragment with config only", () => {
9
- const fragment = defineFragment<{ apiKey: string }>("test");
10
- const testFragment = createFragmentForTest(fragment, [], {
7
+ describe("withTestUtils extension", () => {
8
+ it("should expose deps via services.deps", () => {
9
+ const definition = defineFragment<{ apiKey: string }>("test")
10
+ .withDependencies(({ config }) => ({
11
+ client: { apiKey: config.apiKey },
12
+ }))
13
+ .extend(withTestUtils())
14
+ .build();
15
+
16
+ const fragment = createFragmentForTest(definition, [], {
11
17
  config: { apiKey: "test-key" },
12
18
  });
13
19
 
14
- expect(testFragment.config).toEqual({ apiKey: "test-key" });
15
- expect(testFragment.deps).toEqual({});
16
- expect(testFragment.services).toEqual({});
20
+ expect(fragment.services.deps).toEqual({ client: { apiKey: "test-key" } });
17
21
  });
18
22
 
19
- it("should create deps from fragment definition", () => {
20
- const fragment = defineFragment<{ apiKey: string }>("test").withDependencies(({ config }) => ({
21
- client: { apiKey: config.apiKey },
22
- }));
23
+ it("should work with empty deps", () => {
24
+ const definition = defineFragment<{ value: number }>("test").extend(withTestUtils()).build();
23
25
 
24
- const testFragment = createFragmentForTest(fragment, [], {
25
- config: { apiKey: "test-key" },
26
+ const fragment = createFragmentForTest(definition, [], {
27
+ config: { value: 5 },
26
28
  });
27
29
 
28
- expect(testFragment.deps).toEqual({ client: { apiKey: "test-key" } });
30
+ expect(fragment.services.deps).toEqual({});
29
31
  });
30
32
 
31
- it("should override deps when provided", () => {
32
- const fragment = defineFragment<{ apiKey: string }>("test").withDependencies(({ config }) => ({
33
- client: { apiKey: config.apiKey },
34
- }));
35
-
36
- const testFragment = createFragmentForTest(fragment, [], {
37
- config: { apiKey: "test-key" },
38
- deps: { client: { apiKey: "override-key" } },
39
- });
40
-
41
- expect(testFragment.deps).toEqual({ client: { apiKey: "override-key" } });
33
+ it("should preserve existing base services", () => {
34
+ const definition = defineFragment<{ x: number; y: number }>("test")
35
+ .withDependencies(({ config }) => ({
36
+ x: config.x,
37
+ y: config.y,
38
+ }))
39
+ .providesBaseService(({ deps, defineService }) =>
40
+ defineService({
41
+ add: () => deps.x + deps.y,
42
+ multiply: () => deps.x * deps.y,
43
+ }),
44
+ )
45
+ .extend(withTestUtils())
46
+ .build();
47
+
48
+ const fragment = createFragmentForTest(definition, [], {
49
+ config: { x: 3, y: 4 },
50
+ });
51
+
52
+ // Both existing services and deps should be available
53
+ expect(fragment.services.add()).toBe(7);
54
+ expect(fragment.services.multiply()).toBe(12);
55
+ expect(fragment.services.deps).toEqual({ x: 3, y: 4 });
42
56
  });
43
57
 
44
- it("should create services from fragment definition", () => {
45
- const fragment = defineFragment<{ apiKey: string }>("test")
58
+ it("should work with named services", () => {
59
+ const definition = defineFragment<{ value: number }>("test")
46
60
  .withDependencies(({ config }) => ({
47
- client: { apiKey: config.apiKey },
61
+ value: config.value,
48
62
  }))
49
- .withServices(({ deps }) => ({
50
- getApiKey: () => deps.client.apiKey,
51
- }));
63
+ .providesService("math", ({ deps, defineService }) =>
64
+ defineService({
65
+ double: () => deps.value * 2,
66
+ }),
67
+ )
68
+ .extend(withTestUtils())
69
+ .build();
52
70
 
53
- const testFragment = createFragmentForTest(fragment, [], {
54
- config: { apiKey: "test-key" },
71
+ const fragment = createFragmentForTest(definition, [], {
72
+ config: { value: 21 },
55
73
  });
56
74
 
57
- expect(testFragment.services.getApiKey()).toBe("test-key");
75
+ expect(fragment.services.deps).toEqual({ value: 21 });
76
+ expect(fragment.services.math.double()).toBe(42);
58
77
  });
59
78
 
60
- it("should override services when provided", () => {
61
- const fragment = defineFragment<{ apiKey: string }>("test")
79
+ it("should work with service dependencies", () => {
80
+ type Logger = { log: (msg: string) => void };
81
+
82
+ const logs: string[] = [];
83
+ const mockLogger: Logger = {
84
+ log: (msg: string) => logs.push(msg),
85
+ };
86
+
87
+ const definition = defineFragment<{ name: string }>("test")
62
88
  .withDependencies(({ config }) => ({
63
- client: { apiKey: config.apiKey },
89
+ name: config.name,
64
90
  }))
65
- .withServices(({ deps }) => ({
66
- getApiKey: () => deps.client.apiKey,
67
- }));
68
-
69
- const testFragment = createFragmentForTest(fragment, [], {
70
- config: { apiKey: "test-key" },
71
- services: { getApiKey: () => "override-key" },
91
+ .usesService<"logger", Logger>("logger")
92
+ .providesBaseService(({ deps, serviceDeps, defineService }) =>
93
+ defineService({
94
+ greet: () => {
95
+ serviceDeps.logger.log(`Hello, ${deps.name}!`);
96
+ },
97
+ }),
98
+ )
99
+ .extend(withTestUtils())
100
+ .build();
101
+
102
+ const fragment = createFragmentForTest(definition, [], {
103
+ config: { name: "World" },
104
+ serviceImplementations: {
105
+ logger: mockLogger,
106
+ },
72
107
  });
73
108
 
74
- expect(testFragment.services.getApiKey()).toBe("override-key");
109
+ expect(fragment.services.deps).toEqual({ name: "World" });
110
+ fragment.services.greet();
111
+ expect(logs).toEqual(["Hello, World!"]);
75
112
  });
76
113
 
77
- it("should initialize route factories with fragment context", async () => {
78
- type Config = { multiplier: number };
79
- type Deps = { dep: string };
80
- type Services = { multiply: (x: number) => number };
114
+ it("should work with request storage and context", () => {
115
+ type RequestStorage = { counter: number };
116
+
117
+ const definition = defineFragment<{ initialValue: number }>("test")
118
+ .withDependencies(({ config }) => ({
119
+ initialValue: config.initialValue,
120
+ }))
121
+ .withRequestStorage(
122
+ ({ deps }): RequestStorage => ({
123
+ counter: deps.initialValue,
124
+ }),
125
+ )
126
+ .withThisContext(({ storage }) => {
127
+ const ctx = {
128
+ getCounter: () => storage.getStore()?.counter ?? 0,
129
+ };
130
+ return { serviceContext: ctx, handlerContext: ctx };
131
+ })
132
+ .extend(withTestUtils())
133
+ .build();
134
+
135
+ const fragment = createFragmentForTest(definition, [], {
136
+ config: { initialValue: 10 },
137
+ });
138
+
139
+ expect(fragment.services.deps).toEqual({ initialValue: 10 });
140
+ });
141
+ });
81
142
 
82
- const fragment = defineFragment<Config>("test")
83
- .withDependencies(() => ({ dep: "value" }))
84
- .withServices(({ config }) => ({
85
- multiply: (x: number) => x * config.multiplier,
86
- }));
143
+ describe("createFragmentForTest", () => {
144
+ it("should instantiate fragments with routes", async () => {
145
+ type Config = { multiplier: number };
87
146
 
88
- const routeFactory = defineRoutes<Config, Deps, Services>().create(({ services }) => [
147
+ const definition = defineFragment<Config>("test")
148
+ .withDependencies(({ config }) => ({ multiplier: config.multiplier }))
149
+ .providesService("math", ({ deps, defineService }) =>
150
+ defineService({
151
+ multiply: (x: number) => x * deps.multiplier,
152
+ }),
153
+ )
154
+ .extend(withTestUtils())
155
+ .build();
156
+
157
+ const routeFactory = defineRoutes(definition).create(({ services, defineRoute }) => [
89
158
  defineRoute({
90
159
  method: "GET",
91
160
  path: "/multiply/:num",
92
- outputSchema: z.object({ result: z.number() }),
161
+ outputSchema: z.object({ result: z.number(), deps: z.object({ multiplier: z.number() }) }),
93
162
  handler: async ({ pathParams }, { json }) => {
94
163
  const { num } = pathParams;
95
- return json({ result: services.multiply(Number(num)) });
164
+ return json({
165
+ result: services.math.multiply(Number(num)),
166
+ deps: services.deps,
167
+ });
96
168
  },
97
169
  }),
98
170
  ]);
99
171
 
100
- const routes = [routeFactory] as const;
101
-
102
- const testFragment = createFragmentForTest(fragment, routes, {
172
+ const fragment = createFragmentForTest(definition, [routeFactory], {
103
173
  config: { multiplier: 3 },
104
174
  });
105
175
 
106
- // Test that the route was initialized with the correct services
107
- const response = await testFragment.callRoute("GET", "/multiply/:num", {
108
- // ^?
176
+ const response = await fragment.callRoute("GET", "/multiply/:num", {
109
177
  pathParams: { num: "5" },
110
178
  });
111
- expect(response.type).toBe("json");
112
- if (response.type === "json") {
113
- expect(response.data).toEqual({ result: 15 }); // 5 * 3
114
- expectTypeOf(response.data).toMatchObjectType<{ result: number }>();
115
- }
116
- });
117
- });
118
-
119
- describe("fragment.callRoute", () => {
120
- it("should handle JSON response", async () => {
121
- const fragment = defineFragment<{ apiKey: string }>("test");
122
-
123
- const route = defineRoute({
124
- method: "GET",
125
- path: "/test",
126
- outputSchema: z.object({ message: z.string() }),
127
- handler: async (_ctx, { json }) => {
128
- return json({ message: "hello" });
129
- },
130
- });
131
-
132
- const testFragment = createFragmentForTest(fragment, [route], {
133
- config: { apiKey: "test-key" },
134
- });
135
-
136
- const response = await testFragment.callRoute("GET", "/test");
137
179
 
138
180
  expect(response.type).toBe("json");
139
181
  if (response.type === "json") {
140
- expect(response.status).toBe(200);
141
- expect(response.data).toEqual({ message: "hello" });
142
- expect(response.headers).toBeInstanceOf(Headers);
143
- expectTypeOf(response.data).toMatchObjectType<{ message: string }>();
144
- }
145
- });
146
-
147
- it("should handle empty response", async () => {
148
- const fragment = defineFragment<{ apiKey: string }>("test");
149
-
150
- const route = defineRoute({
151
- method: "DELETE",
152
- path: "/test",
153
- handler: async (_ctx, { empty }) => {
154
- return empty(204);
155
- },
156
- });
157
-
158
- const testFragment = createFragmentForTest(fragment, [route], {
159
- config: { apiKey: "test-key" },
160
- });
161
-
162
- const response = await testFragment.callRoute("DELETE", "/test");
163
-
164
- expect(response.type).toBe("empty");
165
- if (response.type === "empty") {
166
- expect(response.status).toBe(204);
167
- expect(response.headers).toBeInstanceOf(Headers);
182
+ expect(response.data).toEqual({
183
+ result: 15,
184
+ deps: { multiplier: 3 },
185
+ });
168
186
  }
169
187
  });
170
188
 
171
- it("should handle error response", async () => {
172
- const fragment = defineFragment<{ apiKey: string }>("test");
173
-
174
- const route = defineRoute({
175
- method: "GET",
176
- path: "/test",
177
- errorCodes: ["NOT_FOUND"] as const,
178
- handler: async (_ctx, { error }) => {
179
- return error({ message: "Not found", code: "NOT_FOUND" }, 404);
180
- },
181
- });
182
-
183
- const testFragment = createFragmentForTest(fragment, [route], {
184
- config: { apiKey: "test-key" },
185
- });
186
-
187
- const response = await testFragment.callRoute("GET", "/test");
189
+ it("should support request context in routes", async () => {
190
+ type RequestStorage = { counter: number };
188
191
 
189
- expect(response.type).toBe("error");
190
- if (response.type === "error") {
191
- expect(response.status).toBe(404);
192
- expect(response.error).toEqual({ message: "Not found", code: "NOT_FOUND" });
193
- expect(response.headers).toBeInstanceOf(Headers);
194
- }
195
- });
196
-
197
- it("should handle JSON stream response", async () => {
198
- const fragment = defineFragment<{ apiKey: string }>("test");
199
-
200
- const route = defineRoute({
201
- method: "GET",
202
- path: "/test/stream",
203
- outputSchema: z.array(z.object({ value: z.number() })),
204
- handler: async (_ctx, { jsonStream }) => {
205
- return jsonStream(async (stream) => {
206
- for (let i = 1; i <= 5; i++) {
207
- await stream.write({ value: i });
208
- }
209
- });
210
- },
211
- });
212
-
213
- const testFragment = createFragmentForTest(fragment, [route], {
214
- config: { apiKey: "test-key" },
215
- });
216
-
217
- const response = await testFragment.callRoute("GET", "/test/stream");
218
-
219
- expect(response.type).toBe("jsonStream");
220
- if (response.type === "jsonStream") {
221
- expect(response.status).toBe(200);
222
- expect(response.headers).toBeInstanceOf(Headers);
223
- expect(response.headers.get("content-type")).toContain("application/x-ndjson");
224
-
225
- const items = [];
226
- for await (const item of response.stream) {
227
- items.push(item);
228
- }
229
-
230
- expect(items).toEqual([{ value: 1 }, { value: 2 }, { value: 3 }, { value: 4 }, { value: 5 }]);
231
- expectTypeOf(items[0]).toMatchObjectType<{ value: number }>();
232
- }
233
- });
234
-
235
- it("should handle route factory created with defineRoutes", async () => {
236
- const fragment = defineFragment<{ apiKey: string }>("test").withServices(() => ({
237
- getGreeting: (name: string) => `Hello, ${name}!`,
238
- getCount: () => 42,
239
- }));
240
-
241
- type Config = { apiKey: string };
242
- type Deps = {};
243
- type Services = { getGreeting: (name: string) => string; getCount: () => number };
244
-
245
- const routeFactory = defineRoutes<Config, Deps, Services>().create(({ services }) => [
246
- defineRoute({
247
- method: "GET",
248
- path: "/greeting/:name",
249
- outputSchema: z.object({ message: z.string() }),
250
- handler: async ({ pathParams }, { json }) => {
251
- return json({ message: services.getGreeting(pathParams.name) });
252
- },
253
- }),
192
+ const definition = defineFragment<{ initialValue: number }>("test")
193
+ .withDependencies(({ config }) => ({
194
+ initialValue: config.initialValue,
195
+ }))
196
+ .extend(withTestUtils())
197
+ .withRequestStorage(
198
+ ({ deps }): RequestStorage => ({
199
+ counter: deps.initialValue,
200
+ }),
201
+ )
202
+ .withThisContext(({ storage }) => {
203
+ const ctx = {
204
+ getCounter: () => storage.getStore()?.counter ?? 0,
205
+ incrementCounter: () => {
206
+ const store = storage.getStore();
207
+ if (store) {
208
+ store.counter++;
209
+ }
210
+ },
211
+ };
212
+ return { serviceContext: ctx, handlerContext: ctx };
213
+ })
214
+ .build();
215
+
216
+ const routeFactory = defineRoutes(definition).create(({ defineRoute }) => [
254
217
  defineRoute({
255
- method: "GET",
256
- path: "/count",
218
+ method: "POST",
219
+ path: "/increment",
257
220
  outputSchema: z.object({ count: z.number() }),
258
- handler: async (_ctx, { json }) => {
259
- return json({ count: services.getCount() });
221
+ handler: async function (_ctx, { json }) {
222
+ this.incrementCounter();
223
+ return json({ count: this.getCounter() });
260
224
  },
261
225
  }),
262
226
  ]);
263
227
 
264
- const testFragment = createFragmentForTest(fragment, [routeFactory], {
265
- config: { apiKey: "test-key" },
266
- });
267
-
268
- // Test first route
269
- const greetingResponse = await testFragment.callRoute("GET", "/greeting/:name", {
270
- pathParams: { name: "World" },
271
- });
272
-
273
- expect(greetingResponse.type).toBe("json");
274
- if (greetingResponse.type === "json") {
275
- expect(greetingResponse.data).toEqual({ message: "Hello, World!" });
276
- }
277
-
278
- // Test second route
279
- const countResponse = await testFragment.callRoute("GET", "/count");
280
-
281
- expect(countResponse.type).toBe("json");
282
- if (countResponse.type === "json") {
283
- expect(countResponse.data).toEqual({ count: 42 });
284
- }
285
- });
286
-
287
- it("should handle path parameters", async () => {
288
- const fragment = defineFragment<{}>("test");
289
-
290
- const route = defineRoute({
291
- method: "GET",
292
- path: "/users/:id",
293
- outputSchema: z.object({ userId: z.string() }),
294
- handler: async ({ pathParams }, { json }) => {
295
- return json({ userId: pathParams.id });
296
- },
297
- });
298
-
299
- const testFragment = createFragmentForTest(fragment, [route], {
300
- config: {},
301
- });
302
-
303
- const response = await testFragment.callRoute("GET", "/users/:id", {
304
- pathParams: { id: "123" },
228
+ const fragment = createFragmentForTest(definition, [routeFactory], {
229
+ config: { initialValue: 10 },
305
230
  });
306
231
 
307
- expect(response.type).toBe("json");
308
- if (response.type === "json") {
309
- expect(response.data).toEqual({ userId: "123" });
232
+ // Each request should have its own isolated storage
233
+ const response1 = await fragment.callRoute("POST", "/increment");
234
+ expect(response1.type).toBe("json");
235
+ if (response1.type === "json") {
236
+ expect(response1.data).toEqual({ count: 11 });
310
237
  }
311
- });
312
-
313
- it("should handle query parameters", async () => {
314
- const fragment = defineFragment<{}>("test");
315
-
316
- const route = defineRoute({
317
- method: "GET",
318
- path: "/search",
319
- outputSchema: z.object({ query: z.string() }),
320
- handler: async ({ query }, { json }) => {
321
- return json({ query: query.get("q") || "" });
322
- },
323
- });
324
238
 
325
- const testFragment = createFragmentForTest(fragment, [route], {
326
- config: {},
327
- });
328
-
329
- const response = await testFragment.callRoute("GET", "/search", {
330
- query: { q: "test" },
331
- });
332
-
333
- expect(response.type).toBe("json");
334
- if (response.type === "json") {
335
- expect(response.data).toEqual({ query: "test" });
239
+ // New request should start fresh
240
+ const response2 = await fragment.callRoute("POST", "/increment");
241
+ expect(response2.type).toBe("json");
242
+ if (response2.type === "json") {
243
+ expect(response2.data).toEqual({ count: 11 }); // Not 12!
336
244
  }
337
245
  });
338
246
 
339
- it("should handle request body", async () => {
340
- const fragment = defineFragment<{}>("test");
341
-
342
- const route = defineRoute({
343
- method: "POST",
344
- path: "/users",
345
- inputSchema: z.object({ name: z.string(), email: z.string() }),
346
- outputSchema: z.object({ id: z.number(), name: z.string(), email: z.string() }),
347
- handler: async ({ input }, { json }) => {
348
- if (input) {
349
- const data = await input.valid();
350
- return json({ id: 1, name: data.name, email: data.email });
351
- }
352
- return json({ id: 1, name: "", email: "" });
353
- },
354
- });
355
-
356
- const testFragment = createFragmentForTest(fragment, [route], {
357
- config: {},
358
- });
359
-
360
- const response = await testFragment.callRoute("POST", "/users", {
361
- body: { name: "John", email: "john@example.com" },
362
- });
363
-
364
- expect(response.type).toBe("json");
365
- if (response.type === "json") {
366
- expect(response.data).toEqual({ id: 1, name: "John", email: "john@example.com" });
367
- }
368
- });
369
-
370
- it("should have the right types", () => {
371
- const fragment = defineFragment<{}>("test");
372
-
373
- const route = defineRoute({
374
- method: "POST",
375
- path: "/users",
376
- inputSchema: z.object({ name: z.string(), email: z.string() }),
377
- outputSchema: z.object({ id: z.number(), name: z.string(), email: z.string() }),
378
- handler: async ({ input }, { json }) => {
379
- if (input) {
380
- const data = await input.valid();
381
- return json({ id: 1, name: data.name, email: data.email });
382
- }
383
- return json({ id: 1, name: "", email: "" });
384
- },
385
- });
386
-
387
- const testFragment = createFragmentForTest(fragment, [route], {
388
- config: {},
389
- });
390
-
391
- // Check what type body is expected to have
392
- type InputOptions = Parameters<typeof testFragment.callRoute<"POST", "/users">>[2];
393
- type BodyType = NonNullable<NonNullable<InputOptions>["body"]>;
394
-
395
- expectTypeOf<BodyType>().toMatchObjectType<{ name: string; email: string }>();
396
- });
397
-
398
- it("should handle custom headers", async () => {
399
- const fragment = defineFragment<{}>("test");
400
-
401
- const route = defineRoute({
402
- method: "GET",
403
- path: "/test",
404
- outputSchema: z.object({ authHeader: z.string() }),
405
- handler: async ({ headers }, { json }) => {
406
- return json({ authHeader: headers.get("authorization") || "" });
407
- },
408
- });
409
-
410
- const testFragment = createFragmentForTest(fragment, [route], {
411
- config: {},
412
- });
413
-
414
- const response = await testFragment.callRoute("GET", "/test", {
415
- headers: { authorization: "Bearer token" },
416
- });
417
-
418
- expect(response.type).toBe("json");
419
- if (response.type === "json") {
420
- expect(response.data).toEqual({ authHeader: "Bearer token" });
421
- }
422
- });
423
-
424
- it("should properly type path params", async () => {
425
- const fragment = defineFragment<{}>("test");
426
-
427
- const route = defineRoute({
428
- method: "GET",
429
- path: "/orgs/:orgId/users/:userId",
430
- outputSchema: z.object({ orgId: z.string(), userId: z.string() }),
431
- handler: async ({ pathParams }, { json }) => {
432
- return json({ orgId: pathParams.orgId, userId: pathParams.userId });
433
- },
434
- });
435
-
436
- const testFragment = createFragmentForTest(fragment, [route], {
437
- config: {},
438
- });
247
+ it("should work without withTestUtils (no deps exposed)", () => {
248
+ const definition = defineFragment<{ value: number }>("test")
249
+ .withDependencies(({ config }) => ({
250
+ value: config.value,
251
+ }))
252
+ .providesBaseService(({ deps, defineService }) =>
253
+ defineService({
254
+ getValue: () => deps.value,
255
+ }),
256
+ )
257
+ .build();
439
258
 
440
- const response = await testFragment.callRoute("GET", "/orgs/:orgId/users/:userId", {
441
- pathParams: { orgId: "123", userId: "456" },
259
+ const fragment = createFragmentForTest(definition, [], {
260
+ config: { value: 42 },
442
261
  });
443
262
 
444
- expect(response.type).toBe("json");
445
- if (response.type === "json") {
446
- expect(response.data).toEqual({ orgId: "123", userId: "456" });
447
- }
263
+ // Services should work
264
+ expect(fragment.services.getValue()).toBe(42);
265
+ // But deps should not be exposed
266
+ expect(fragment.services).not.toHaveProperty("deps");
448
267
  });
449
268
  });