@fragno-dev/core 0.1.8 → 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 (176) hide show
  1. package/.turbo/turbo-build.log +131 -56
  2. package/CHANGELOG.md +13 -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 -3
  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/error.js +48 -0
  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 -2
  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 -2
  71. package/dist/client/client.d.ts.map +1 -0
  72. package/dist/client/client.js +397 -5
  73. package/dist/client/client.js.map +1 -0
  74. package/dist/client/client.svelte.d.ts +5 -2
  75. package/dist/client/client.svelte.d.ts.map +1 -1
  76. package/dist/client/client.svelte.js +1 -4
  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 -2
  83. package/dist/client/react.d.ts.map +1 -1
  84. package/dist/client/react.js +3 -4
  85. package/dist/client/react.js.map +1 -1
  86. package/dist/client/solid.d.ts +5 -2
  87. package/dist/client/solid.d.ts.map +1 -1
  88. package/dist/client/solid.js +2 -4
  89. package/dist/client/solid.js.map +1 -1
  90. package/dist/client/vanilla.d.ts +5 -2
  91. package/dist/client/vanilla.d.ts.map +1 -1
  92. package/dist/client/vanilla.js +2 -42
  93. package/dist/client/vanilla.js.map +1 -1
  94. package/dist/client/vue.d.ts +5 -2
  95. package/dist/client/vue.d.ts.map +1 -1
  96. package/dist/client/vue.js +1 -4
  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 -2
  110. package/dist/mod.js +4 -4
  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 -34
  114. package/dist/test/test.d.ts.map +1 -1
  115. package/dist/test/test.js +75 -42
  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-kyKI7pqH.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 +1 -5
  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 +454 -189
  136. package/src/api/request-context-storage.ts +64 -0
  137. package/src/api/request-middleware.test.ts +301 -228
  138. package/src/api/route.test.ts +12 -36
  139. package/src/api/route.ts +167 -155
  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 +22 -15
  154. package/src/request/request.ts +8 -0
  155. package/src/test/test.test.ts +189 -375
  156. package/src/test/test.ts +186 -152
  157. package/tsdown.config.ts +8 -5
  158. package/dist/api/fragment-builder.d.ts +0 -2
  159. package/dist/api/fragment-builder.js +0 -3
  160. package/dist/api/fragment-instantiation.d.ts +0 -2
  161. package/dist/api/fragment-instantiation.js +0 -4
  162. package/dist/api-BFrUCIsF.d.ts +0 -963
  163. package/dist/api-BFrUCIsF.d.ts.map +0 -1
  164. package/dist/client-DAFHcKqA.js +0 -782
  165. package/dist/client-DAFHcKqA.js.map +0 -1
  166. package/dist/fragment-builder-Boh2vNHq.js +0 -108
  167. package/dist/fragment-builder-Boh2vNHq.js.map +0 -1
  168. package/dist/fragment-instantiation-DUT-HLl1.js +0 -898
  169. package/dist/fragment-instantiation-DUT-HLl1.js.map +0 -1
  170. package/dist/route-C4CyNHkC.js +0 -26
  171. package/dist/route-C4CyNHkC.js.map +0 -1
  172. package/dist/ssr-kyKI7pqH.js.map +0 -1
  173. package/src/api/fragment-builder.ts +0 -518
  174. package/src/api/fragment-instantiation.test.ts +0 -702
  175. package/src/api/fragment-instantiation.ts +0 -766
  176. package/src/api/fragment.test.ts +0 -585
@@ -1,585 +0,0 @@
1
- import { test, expect, describe, expectTypeOf } from "vitest";
2
- import { defineFragment, type FragmentBuilder } from "./fragment-builder";
3
- import { createFragment } from "./fragment-instantiation";
4
- import { defineRoute, defineRoutes, type RouteFactory, resolveRouteFactories } from "./route";
5
- import { z } from "zod";
6
- import type { InferOr } from "../util/types-util";
7
-
8
- type Empty = Record<never, never>;
9
-
10
- describe("new-fragment API", () => {
11
- describe("Type inference", () => {
12
- test("defineFragment infers config type correctly", () => {
13
- const _config = {
14
- apiKey: "test-key",
15
- maxRetries: 3,
16
- debug: false,
17
- };
18
-
19
- const _fragment = defineFragment<typeof _config>("test");
20
-
21
- expectTypeOf<typeof _fragment>().toEqualTypeOf<
22
- FragmentBuilder<
23
- {
24
- apiKey: string;
25
- maxRetries: number;
26
- debug: boolean;
27
- },
28
- Empty,
29
- Empty,
30
- Empty
31
- >
32
- >();
33
- });
34
-
35
- test("withDependencies correctly transforms dependency types", () => {
36
- const _config = {
37
- apiKey: "test-key",
38
- };
39
-
40
- const _fragment = defineFragment<typeof _config>("test").withDependencies(({ config }) => {
41
- expectTypeOf(config).toEqualTypeOf<typeof _config>();
42
- return {
43
- httpClient: { fetch: () => Promise.resolve(new Response()) },
44
- logger: { log: (msg: string) => console.log(msg) },
45
- };
46
- });
47
-
48
- expectTypeOf<typeof _fragment>().toEqualTypeOf<
49
- FragmentBuilder<
50
- typeof _config,
51
- {
52
- httpClient: { fetch: () => Promise<Response> };
53
- logger: { log: (msg: string) => void };
54
- },
55
- Empty,
56
- Empty
57
- >
58
- >();
59
- });
60
-
61
- test("providesService has access to dependencies and config", () => {
62
- const _config = {
63
- apiKey: "test-key",
64
- baseUrl: "https://api.example.com",
65
- };
66
-
67
- const _fragment = defineFragment<typeof _config>("test")
68
- .withDependencies(({ config }) => {
69
- expectTypeOf(config).toEqualTypeOf<{
70
- apiKey: string;
71
- baseUrl: string;
72
- }>();
73
-
74
- return { httpClient: { baseUrl: config.baseUrl } };
75
- })
76
- .providesService(({ config, deps, defineService }) => {
77
- expectTypeOf(config).toEqualTypeOf<typeof _config>();
78
- expectTypeOf(deps).toEqualTypeOf<{ httpClient: { baseUrl: string } }>();
79
-
80
- return defineService({
81
- userService: {
82
- getUser: async (id: string) => ({ id, name: "Test User" }),
83
- },
84
- cacheService: {
85
- get: (_key: string): string => crypto.randomUUID(),
86
- set: (_key: string, _value: string) => {},
87
- },
88
- });
89
- });
90
-
91
- expectTypeOf<typeof _fragment>().toEqualTypeOf<
92
- FragmentBuilder<
93
- typeof _config,
94
- { httpClient: { baseUrl: string } },
95
- {
96
- userService: {
97
- getUser: (id: string) => Promise<{ id: string; name: string }>;
98
- };
99
- cacheService: {
100
- get: (key: string) => string;
101
- set: (key: string, value: string) => void;
102
- };
103
- },
104
- Empty
105
- >
106
- >();
107
- });
108
-
109
- test("defineRoutes receives correct context types", () => {
110
- type Config = {
111
- apiKey: string;
112
- model: "gpt-3" | "gpt-4";
113
- };
114
-
115
- type Deps = {
116
- openai: { complete: (prompt: string) => Promise<string> };
117
- };
118
-
119
- type Services = {
120
- cache: Map<string, unknown>;
121
- };
122
-
123
- const _routeFactory = defineRoutes<Config, Deps, Services>().create(
124
- ({ config, deps, services }) => {
125
- expectTypeOf(config).toEqualTypeOf<Config>();
126
- expectTypeOf(deps).toEqualTypeOf<Deps>();
127
- expectTypeOf(services).toEqualTypeOf<Services>();
128
-
129
- return [
130
- defineRoute({
131
- method: "POST",
132
- path: "/complete",
133
- inputSchema: z.object({ prompt: z.string() }),
134
- outputSchema: z.object({ result: z.string() }),
135
- handler: async ({ input }, { json }) => {
136
- const { prompt } = await input.valid();
137
- expectTypeOf(prompt).toEqualTypeOf<string>();
138
- expectTypeOf<Parameters<typeof json>[0]>().toEqualTypeOf<{ result: string }>();
139
-
140
- const result = await deps.openai.complete(prompt);
141
- services.cache.set(prompt, result);
142
- return json({ result });
143
- },
144
- }),
145
- ];
146
- },
147
- );
148
-
149
- expectTypeOf<Parameters<typeof _routeFactory>[0]>().toEqualTypeOf<{
150
- config: Config;
151
- deps: Deps;
152
- services: Services;
153
- }>();
154
- });
155
- });
156
-
157
- describe("Builder pattern", () => {
158
- test("Builder methods return new instances", () => {
159
- const _config = { test: true };
160
-
161
- const lib1 = defineFragment<typeof _config>("test");
162
- expectTypeOf(lib1).toEqualTypeOf<FragmentBuilder<typeof _config, Empty, Empty, Empty>>();
163
-
164
- const lib2 = lib1.withDependencies(() => ({ dep1: "value1" }));
165
- expectTypeOf(lib2).toEqualTypeOf<
166
- FragmentBuilder<typeof _config, { dep1: string }, Empty, Empty>
167
- >();
168
- const lib3 = lib2.providesService(({ defineService }) =>
169
- defineService({ service1: "value1" }),
170
- );
171
- expectTypeOf(lib3).toEqualTypeOf<
172
- FragmentBuilder<typeof _config, { dep1: string }, { service1: string }, Empty>
173
- >();
174
-
175
- expect(lib1).not.toBe(lib2);
176
- expect(lib2).not.toBe(lib3);
177
- expect(lib1).not.toBe(lib3);
178
- });
179
-
180
- test("Each builder step preserves previous configuration", () => {
181
- const _config = { apiKey: "test" };
182
-
183
- const fragment = defineFragment<typeof _config>("my-lib")
184
- .withDependencies(({ config }) => ({
185
- client: `Client for ${config.apiKey}`,
186
- }))
187
- .providesService(({ deps, defineService }) =>
188
- defineService({
189
- service: `Service using ${deps.client}`,
190
- }),
191
- );
192
-
193
- expect(fragment.definition.name).toBe("my-lib");
194
- expect(fragment.definition.dependencies).toBeDefined();
195
- expect(fragment.definition.services).toBeDefined();
196
- });
197
- });
198
-
199
- describe("Fragment creation", () => {
200
- test("createFragment instantiates fragment with config", async () => {
201
- const InputSchema = z.object({ name: z.string() });
202
- const OutputSchema = z.object({ greeting: z.string() });
203
-
204
- const routeFactory = defineRoutes<
205
- { prefix: string },
206
- { formatter: (s: string) => string },
207
- { logger: { log: (s: string) => void } }
208
- >().create(({ config, deps, services }) => [
209
- defineRoute({
210
- method: "POST",
211
- path: "/greet",
212
- inputSchema: InputSchema,
213
- outputSchema: OutputSchema,
214
- handler: async ({ input }, { json }) => {
215
- const { name } = await input.valid();
216
- const greeting = deps.formatter(`${config.prefix} ${name}`);
217
- services.logger.log(greeting);
218
- return json({ greeting });
219
- },
220
- }),
221
- ]);
222
-
223
- const fragmentDef = defineFragment("greeting")
224
- .withDependencies(() => ({
225
- formatter: (s: string) => s.toUpperCase(),
226
- }))
227
- .providesService(({ defineService }) =>
228
- defineService({
229
- logger: { log: (s: string) => console.log(s) },
230
- }),
231
- );
232
-
233
- const fragment = createFragment(fragmentDef, { prefix: "Hello" }, [routeFactory], {});
234
-
235
- expect(fragment.mountRoute).toBe("/api/greeting");
236
- expect(fragment.config.name).toBe("greeting");
237
- expect(fragment.services).toHaveProperty("logger");
238
- expect(fragment.handler).toBeInstanceOf(Function);
239
-
240
- const request = new Request("http://localhost/api/greeting/greet", {
241
- method: "POST",
242
- body: JSON.stringify({ name: "World" }),
243
- headers: { "Content-Type": "application/json" },
244
- });
245
-
246
- const response = await fragment.handler(request);
247
- expect(response.status).toBe(200);
248
-
249
- const data = await response.json();
250
- expect(data).toEqual({ greeting: "HELLO WORLD" });
251
- });
252
-
253
- test("Wildcard path", async () => {
254
- const route = defineRoute({
255
- method: "GET",
256
- path: "/thing/:id/**:path",
257
- handler: async ({ pathParams: _pathParams }, outputCtx) => {
258
- expectTypeOf<typeof _pathParams>().toEqualTypeOf<{ id: string; path: string }>();
259
- return outputCtx.json({ message: "Hello, World!" });
260
- },
261
- });
262
-
263
- const fragmentDef = defineFragment("test-fragment");
264
- const fragment = createFragment(fragmentDef, {}, [route], {
265
- mountRoute: "/api",
266
- });
267
-
268
- // Create a test request
269
- const request = new Request("http://localhost:3000/api/thing/123/foo/bar", {
270
- method: "GET",
271
- });
272
-
273
- // Call the handler
274
- const response = await fragment.handler(request);
275
-
276
- // Verify the response
277
- expect(response.status).toBe(200);
278
- const data = await response.json();
279
- expect(data).toEqual({ message: "Hello, World!" });
280
- });
281
-
282
- test("Routes receive correct context from fragment definition", async () => {
283
- let capturedConfig;
284
- let capturedDeps;
285
- let capturedServices;
286
-
287
- const routeFactory = defineRoutes<
288
- { setting: string },
289
- { tool: string },
290
- { storage: string }
291
- >().create(({ config, deps, services }) => {
292
- capturedConfig = config;
293
- capturedDeps = deps;
294
- capturedServices = services;
295
- return [
296
- defineRoute({
297
- method: "GET",
298
- path: "/test",
299
- handler: async (_, { json }) => json({ ok: true }),
300
- }),
301
- ];
302
- });
303
-
304
- const fragmentDef = defineFragment("test")
305
- .withDependencies(() => ({ tool: "hammer" }))
306
- .providesService(({ defineService }) => defineService({ storage: "memory" }));
307
-
308
- createFragment(fragmentDef, { setting: "value" }, [routeFactory], {});
309
-
310
- expect(capturedConfig).toEqual({ setting: "value" });
311
- expect(capturedDeps).toEqual({ tool: "hammer" });
312
- expect(capturedServices).toEqual({ storage: "memory" });
313
- });
314
- });
315
-
316
- describe("Type constraints", () => {
317
- test("Services must extend Record<string, unknown>", () => {
318
- const fragmentDef = defineFragment("test").providesService(({ defineService }) =>
319
- defineService({
320
- validService: { method: () => {} },
321
- anotherService: "string value",
322
- numberService: 123,
323
- }),
324
- );
325
-
326
- const _fragment = createFragment(fragmentDef, {}, [], {});
327
-
328
- expectTypeOf<typeof _fragment.services>().toEqualTypeOf<{
329
- validService: { method: () => void };
330
- anotherService: string;
331
- numberService: number;
332
- }>();
333
- });
334
-
335
- test("Route handler types are preserved", () => {
336
- const OutputSchema = z.object({ data: z.string() });
337
-
338
- const route = defineRoute({
339
- method: "GET",
340
- path: "/item/:id",
341
- inputSchema: z.object({ id: z.number() }),
342
- outputSchema: OutputSchema,
343
- errorCodes: ["NOT_FOUND"],
344
- queryParameters: ["page", "limit"],
345
- handler: async ({ pathParams, input }, { json }) => {
346
- expectTypeOf(pathParams).toEqualTypeOf<{ id: string }>();
347
- const validated = await input.valid();
348
- expectTypeOf(validated).toEqualTypeOf<{ id: number }>();
349
- return json({ data: "test" });
350
- },
351
- });
352
- expectTypeOf(route.method).toEqualTypeOf<"GET">();
353
- expectTypeOf(route.path).toEqualTypeOf<"/item/:id">();
354
- expectTypeOf<InferOr<typeof route.inputSchema, undefined>>().toEqualTypeOf<
355
- { id: number } | undefined
356
- >();
357
- expectTypeOf(route.outputSchema).toEqualTypeOf<typeof OutputSchema | undefined>();
358
- expectTypeOf(route.errorCodes).toEqualTypeOf<readonly "NOT_FOUND"[] | undefined>();
359
- expectTypeOf(route.queryParameters).toEqualTypeOf<
360
- readonly ("page" | "limit")[] | undefined
361
- >();
362
- });
363
- });
364
-
365
- describe("resolveRouteFactories", () => {
366
- test("resolveRouteFactories returns correct routes", () => {
367
- const routeFactory = defineRoutes().create(() => {
368
- const firstRoute = defineRoute({
369
- method: "GET",
370
- path: "/first",
371
- inputSchema: z.object({ id: z.string() }),
372
- outputSchema: z.object({ ok: z.boolean() }),
373
- errorCodes: ["FIRST_NOT_FOUND"],
374
- queryParameters: ["page", "limit"],
375
- handler: async (_, { json }) => json({ ok: true }),
376
- });
377
-
378
- return [
379
- firstRoute,
380
- defineRoute({
381
- method: "POST",
382
- path: "/second",
383
- inputSchema: z.object({ id: z.string() }),
384
- outputSchema: z.object({ ok: z.boolean() }),
385
- errorCodes: ["SECOND_NOT_FOUND"],
386
- queryParameters: ["page", "limit"],
387
- handler: async (_, { json }) => json({ ok: true }),
388
- }),
389
- ];
390
- });
391
-
392
- type RouteFactoryRoutes =
393
- typeof routeFactory extends RouteFactory<infer _T1, infer _T2, infer _T3, infer TRoutes>
394
- ? TRoutes
395
- : never;
396
-
397
- expectTypeOf<RouteFactoryRoutes[0]["path"]>().toEqualTypeOf<"/first">();
398
- expectTypeOf<RouteFactoryRoutes[0]["method"]>().toEqualTypeOf<"GET">();
399
- expectTypeOf<RouteFactoryRoutes[0]["errorCodes"]>().toEqualTypeOf<
400
- readonly "FIRST_NOT_FOUND"[] | undefined
401
- >();
402
- expectTypeOf<RouteFactoryRoutes[0]["queryParameters"]>().toEqualTypeOf<
403
- readonly ("page" | "limit")[] | undefined
404
- >();
405
-
406
- expectTypeOf<RouteFactoryRoutes[1]["path"]>().toEqualTypeOf<"/second">();
407
- expectTypeOf<RouteFactoryRoutes[1]["method"]>().toEqualTypeOf<"POST">();
408
- expectTypeOf<RouteFactoryRoutes[1]["errorCodes"]>().toEqualTypeOf<
409
- readonly "SECOND_NOT_FOUND"[] | undefined
410
- >();
411
- expectTypeOf<RouteFactoryRoutes[1]["queryParameters"]>().toEqualTypeOf<
412
- readonly ("page" | "limit")[] | undefined
413
- >();
414
-
415
- const routes = resolveRouteFactories(
416
- {
417
- config: {},
418
- deps: {},
419
- services: {},
420
- },
421
- [routeFactory],
422
- );
423
-
424
- const [r1, r2] = routes;
425
- {
426
- const { path, method, errorCodes, queryParameters } = r1;
427
- expectTypeOf(path).toEqualTypeOf<"/first">();
428
- expectTypeOf(method).toEqualTypeOf<"GET">();
429
- expectTypeOf(errorCodes).toEqualTypeOf<readonly "FIRST_NOT_FOUND"[] | undefined>();
430
- expectTypeOf(queryParameters).toEqualTypeOf<readonly ("page" | "limit")[] | undefined>();
431
-
432
- expect(path).toBe("/first");
433
- expect(method).toBe("GET");
434
- expect(errorCodes).toEqual(["FIRST_NOT_FOUND"]);
435
- expect(queryParameters).toEqual(["page", "limit"]);
436
- }
437
-
438
- {
439
- const { path, method, errorCodes, queryParameters } = r2;
440
- expectTypeOf(path).toEqualTypeOf<"/second">();
441
- expectTypeOf(method).toEqualTypeOf<"POST">();
442
- expectTypeOf(errorCodes).toEqualTypeOf<readonly "SECOND_NOT_FOUND"[] | undefined>();
443
- expectTypeOf(queryParameters).toEqualTypeOf<readonly ("page" | "limit")[] | undefined>();
444
-
445
- expect(path).toBe("/second");
446
- expect(method).toBe("POST");
447
- expect(errorCodes).toEqual(["SECOND_NOT_FOUND"]);
448
- expect(queryParameters).toEqual(["page", "limit"]);
449
- }
450
- });
451
-
452
- test("defineRoutes preserves route types with explicit context types", () => {
453
- type Config = {
454
- apiKey: string;
455
- model: "gpt-3" | "gpt-4";
456
- };
457
-
458
- type Deps = {
459
- openai: { complete: (prompt: string) => Promise<string> };
460
- };
461
-
462
- type Services = {
463
- cache: Map<string, unknown>;
464
- };
465
-
466
- const routeFactory = defineRoutes<Config, Deps, Services>().create(
467
- ({ config, deps, services }) => {
468
- expectTypeOf(config).toEqualTypeOf<Config>();
469
- expectTypeOf(deps).toEqualTypeOf<Deps>();
470
- expectTypeOf(services).toEqualTypeOf<Services>();
471
-
472
- return [
473
- defineRoute({
474
- method: "POST",
475
- path: "/complete",
476
- inputSchema: z.object({ prompt: z.string() }),
477
- outputSchema: z.object({ result: z.string() }),
478
- errorCodes: ["RATE_LIMITED"],
479
- handler: async ({ input }, { json }) => {
480
- const { prompt } = await input.valid();
481
- const result = await deps.openai.complete(prompt);
482
- services.cache.set(prompt, result);
483
- return json({ result });
484
- },
485
- }),
486
- defineRoute({
487
- method: "GET",
488
- path: "/status",
489
- outputSchema: z.object({ status: z.literal("ok") }),
490
- handler: async (_, { json }) => json({ status: "ok" }),
491
- }),
492
- ];
493
- },
494
- );
495
-
496
- type RouteFactoryRoutes =
497
- typeof routeFactory extends RouteFactory<infer _T1, infer _T2, infer _T3, infer TRoutes>
498
- ? TRoutes
499
- : never;
500
-
501
- expectTypeOf<RouteFactoryRoutes[0]["path"]>().toEqualTypeOf<"/complete">();
502
- expectTypeOf<RouteFactoryRoutes[0]["method"]>().toEqualTypeOf<"POST">();
503
- expectTypeOf<RouteFactoryRoutes[0]["errorCodes"]>().toEqualTypeOf<
504
- readonly "RATE_LIMITED"[] | undefined
505
- >();
506
-
507
- expectTypeOf<RouteFactoryRoutes[1]["path"]>().toEqualTypeOf<"/status">();
508
- expectTypeOf<RouteFactoryRoutes[1]["method"]>().toEqualTypeOf<"GET">();
509
-
510
- const routes = resolveRouteFactories(
511
- {
512
- config: { apiKey: "test", model: "gpt-4" as const },
513
- deps: { openai: { complete: async () => "result" } },
514
- services: { cache: new Map() },
515
- },
516
- [routeFactory],
517
- );
518
-
519
- expectTypeOf(routes[0].path).toEqualTypeOf<"/complete">();
520
- expectTypeOf(routes[1].path).toEqualTypeOf<"/status">();
521
- });
522
- });
523
-
524
- describe("Database Integration", () => {
525
- test("createFragment without database works without adapter", () => {
526
- const fragmentDef = defineFragment("test").withDependencies(() => ({
527
- service: { data: "test" },
528
- }));
529
-
530
- const fragment = createFragment(fragmentDef, {}, [], {});
531
-
532
- expect(fragment.deps.service.data).toBe("test");
533
- });
534
-
535
- test("createFragment accepts options parameter", () => {
536
- const fragmentDef = defineFragment("test").withDependencies(() => ({
537
- service: { data: "test" },
538
- }));
539
-
540
- const fragment = createFragment(fragmentDef, {}, [], { mountRoute: "/custom" });
541
-
542
- expect(fragment.mountRoute).toBe("/custom");
543
- });
544
- });
545
-
546
- describe("Route handler this context", () => {
547
- test("this context type is RequestThisContext for standard fragments", () => {
548
- const fragmentDef = defineFragment("test");
549
-
550
- const routesFactory = defineRoutes().create(() => {
551
- return [
552
- defineRoute({
553
- method: "GET",
554
- path: "/test",
555
- handler: async function (_, { json }) {
556
- // this should be RequestThisContext
557
- // (we can't easily test the exact type due to how TypeScript handles 'this')
558
- expect(this).toBeDefined();
559
- expect(typeof this).toBe("object");
560
- return json({ ok: true });
561
- },
562
- }),
563
- ];
564
- });
565
-
566
- const _fragment = createFragment(fragmentDef, {}, [routesFactory], {});
567
- expect(_fragment).toBeDefined();
568
- });
569
-
570
- test("defineRoute without defineRoutes defaults to RequestThisContext", () => {
571
- const route = defineRoute({
572
- method: "GET",
573
- path: "/test",
574
- handler: async function (_, { json }) {
575
- // this defaults to RequestThisContext
576
- expect(this).toBeDefined();
577
- expect(typeof this).toBe("object");
578
- return json({ ok: true });
579
- },
580
- });
581
-
582
- expect(route).toBeDefined();
583
- });
584
- });
585
- });