@fragno-dev/core 0.0.1

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