@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,493 @@
1
+ import { test, expectTypeOf, describe } from "vitest";
2
+ import { z } from "zod";
3
+ import { type FragnoRouteConfig, type HTTPMethod } from "../api/api";
4
+ import { defineRoute } from "../api/route";
5
+ import type {
6
+ ExtractGetRoutes,
7
+ ExtractGetRoutePaths,
8
+ ExtractOutputSchemaForPath,
9
+ ExtractRouteByPath,
10
+ IsValidGetRoutePath,
11
+ ValidateGetRoutePath,
12
+ HasGetRoutes,
13
+ FragnoClientMutatorData,
14
+ } from "./client";
15
+ import type { StandardSchemaV1 } from "@standard-schema/spec";
16
+
17
+ // Test route configurations for type testing
18
+ const _testRoutes = [
19
+ // GET routes
20
+ defineRoute({
21
+ method: "GET",
22
+ path: "/home",
23
+ handler: async (_ctx, { json }) => json({}),
24
+ }),
25
+ defineRoute({
26
+ method: "GET",
27
+ path: "/users",
28
+ outputSchema: z.array(z.object({ id: z.number(), name: z.string() })),
29
+ handler: async (_ctx, { json }) => {
30
+ return json([{ id: 1, name: "" } as const]);
31
+ },
32
+ }),
33
+ defineRoute({
34
+ method: "GET",
35
+ path: "/users/:id",
36
+ outputSchema: z.object({ id: z.number(), name: z.string() }),
37
+ handler: async ({ pathParams }, { json }) => {
38
+ return json({ id: Number(pathParams.id), name: "" } as const);
39
+ },
40
+ }),
41
+ defineRoute({
42
+ method: "GET",
43
+ path: "/posts/:postId/comments",
44
+ outputSchema: z.array(z.object({ id: z.number(), content: z.string() })),
45
+ handler: async (_ctx, { json }) => json([]),
46
+ }),
47
+ defineRoute({
48
+ method: "GET",
49
+ path: "/static/**:path",
50
+ handler: async (_ctx, { json }) => json({}),
51
+ }),
52
+ // Non-GET routes (should be filtered out)
53
+ defineRoute({
54
+ method: "POST",
55
+ path: "/users",
56
+ inputSchema: z.object({ name: z.string() }),
57
+ outputSchema: z.object({ id: z.number(), name: z.string() }),
58
+ handler: async (_ctx, { json }) => json({ id: 1, name: "" }),
59
+ }),
60
+ defineRoute({
61
+ method: "PUT",
62
+ path: "/users/:id",
63
+ inputSchema: z.object({ name: z.string() }),
64
+ handler: async (_ctx, { json }) => json({}),
65
+ }),
66
+ defineRoute({
67
+ method: "DELETE",
68
+ path: "/users/:id",
69
+ handler: async (_ctx, { json }) => json({}),
70
+ }),
71
+ ] as const;
72
+
73
+ // Empty routes array for edge case testing
74
+ const _emptyRoutes = [] as const satisfies readonly FragnoRouteConfig<
75
+ HTTPMethod,
76
+ string,
77
+ StandardSchemaV1 | undefined,
78
+ StandardSchemaV1 | undefined,
79
+ string,
80
+ string
81
+ >[];
82
+
83
+ // Routes with no GET methods
84
+ const _noGetRoutes = [
85
+ defineRoute({
86
+ method: "POST",
87
+ path: "/create",
88
+ handler: async (_ctx, { json }) => json({}),
89
+ }),
90
+ defineRoute({
91
+ method: "DELETE",
92
+ path: "/delete/:id",
93
+ handler: async (_ctx, { json }) => json({}),
94
+ }),
95
+ ] as const;
96
+
97
+ test("ExtractGetRoutes type tests", () => {
98
+ // Should extract only GET routes from mixed routes
99
+ type GetRoutes = ExtractGetRoutes<typeof _testRoutes>;
100
+
101
+ // The result should be an array of only GET route configs
102
+ // We can't directly test the array structure, but we can verify it contains the right routes
103
+ expectTypeOf<GetRoutes>().toBeArray();
104
+
105
+ // Should be empty array for routes with no GET methods
106
+ type NoGetRoutesResult = ExtractGetRoutes<typeof _noGetRoutes>;
107
+ expectTypeOf<NoGetRoutesResult>().toEqualTypeOf<never[]>();
108
+
109
+ // Should be empty array for empty routes
110
+ type EmptyRoutesResult = ExtractGetRoutes<typeof _emptyRoutes>;
111
+ expectTypeOf<EmptyRoutesResult>().toEqualTypeOf<never[]>();
112
+ });
113
+
114
+ test("ExtractGetRoutePaths type tests", () => {
115
+ // Should extract only paths from GET routes
116
+ type GetPaths = ExtractGetRoutePaths<typeof _testRoutes>;
117
+ expectTypeOf<GetPaths>().toEqualTypeOf<
118
+ "/home" | "/users" | "/users/:id" | "/posts/:postId/comments" | "/static/**:path"
119
+ >();
120
+
121
+ // Should be never for routes with no GET methods
122
+ type NoGetPaths = ExtractGetRoutePaths<typeof _noGetRoutes>;
123
+ expectTypeOf<NoGetPaths>().toEqualTypeOf<never>();
124
+
125
+ // Should be never for empty routes
126
+ type EmptyPaths = ExtractGetRoutePaths<typeof _emptyRoutes>;
127
+ expectTypeOf<EmptyPaths>().toEqualTypeOf<never>();
128
+ });
129
+
130
+ test("ExtractOutputSchemaForPath type tests", () => {
131
+ // Should extract correct output schema for existing GET route
132
+ type UsersSchema = ExtractOutputSchemaForPath<typeof _testRoutes, "/users">;
133
+ expectTypeOf<UsersSchema>().toEqualTypeOf<
134
+ z.ZodArray<
135
+ z.ZodObject<{
136
+ id: z.ZodNumber;
137
+ name: z.ZodString;
138
+ }>
139
+ >
140
+ >();
141
+
142
+ // Should extract correct output schema for parameterized route
143
+ type UserSchema = ExtractOutputSchemaForPath<typeof _testRoutes, "/users/:id">;
144
+ expectTypeOf<UserSchema>().toEqualTypeOf<
145
+ z.ZodObject<{
146
+ id: z.ZodNumber;
147
+ name: z.ZodString;
148
+ }>
149
+ >();
150
+
151
+ // Note: Routes without output schema have complex type inference, skipping direct test
152
+
153
+ // Should be never for non-existent path
154
+ type NonExistentSchema = ExtractOutputSchemaForPath<typeof _testRoutes, "/nonexistent">;
155
+ expectTypeOf<NonExistentSchema>().toEqualTypeOf<never>();
156
+
157
+ type PathWithNoSchema = ExtractOutputSchemaForPath<typeof _testRoutes, "/home">;
158
+ expectTypeOf<PathWithNoSchema>().toEqualTypeOf<StandardSchemaV1<unknown, unknown> | undefined>();
159
+ });
160
+
161
+ test("IsValidGetRoutePath type tests", () => {
162
+ // Should return true for valid GET route paths
163
+ expectTypeOf<IsValidGetRoutePath<typeof _testRoutes, "/home">>().toEqualTypeOf<true>();
164
+ expectTypeOf<IsValidGetRoutePath<typeof _testRoutes, "/users">>().toEqualTypeOf<true>();
165
+ expectTypeOf<IsValidGetRoutePath<typeof _testRoutes, "/users/:id">>().toEqualTypeOf<true>();
166
+ expectTypeOf<
167
+ IsValidGetRoutePath<typeof _testRoutes, "/posts/:postId/comments">
168
+ >().toEqualTypeOf<true>();
169
+ expectTypeOf<IsValidGetRoutePath<typeof _testRoutes, "/static/**:path">>().toEqualTypeOf<true>();
170
+
171
+ // Should return false for non-GET routes (even if they exist)
172
+ expectTypeOf<IsValidGetRoutePath<typeof _testRoutes, "/users">>().toEqualTypeOf<true>(); // This is GET
173
+
174
+ // Should return false for non-existent paths
175
+ expectTypeOf<IsValidGetRoutePath<typeof _testRoutes, "/nonexistent">>().toEqualTypeOf<false>();
176
+ expectTypeOf<IsValidGetRoutePath<typeof _testRoutes, "/admin">>().toEqualTypeOf<false>();
177
+
178
+ // Should return false for empty or no-GET routes
179
+ expectTypeOf<IsValidGetRoutePath<typeof _emptyRoutes, "/anything">>().toEqualTypeOf<false>();
180
+ expectTypeOf<IsValidGetRoutePath<typeof _noGetRoutes, "/create">>().toEqualTypeOf<false>();
181
+ });
182
+
183
+ test("ValidateGetRoutePath type tests", () => {
184
+ // Should return the path itself for valid GET routes
185
+ expectTypeOf<ValidateGetRoutePath<typeof _testRoutes, "/home">>().toEqualTypeOf<"/home">();
186
+ expectTypeOf<ValidateGetRoutePath<typeof _testRoutes, "/users">>().toEqualTypeOf<"/users">();
187
+ expectTypeOf<
188
+ ValidateGetRoutePath<typeof _testRoutes, "/users/:id">
189
+ >().toEqualTypeOf<"/users/:id">();
190
+
191
+ // Should return error message for invalid paths
192
+ type InvalidPathError = ValidateGetRoutePath<typeof _testRoutes, "/nonexistent">;
193
+ expectTypeOf<InvalidPathError>().toMatchTypeOf<string>();
194
+
195
+ // Should return error for POST/PUT/DELETE routes even if they exist
196
+ type PostRouteError = ValidateGetRoutePath<typeof _testRoutes, "/users">;
197
+ expectTypeOf<PostRouteError>().toEqualTypeOf<"/users">(); // This is actually a GET route
198
+ });
199
+
200
+ test("HasGetRoutes type tests", () => {
201
+ // Should return true for routes that contain GET methods
202
+ expectTypeOf<HasGetRoutes<typeof _testRoutes>>().toEqualTypeOf<true>();
203
+
204
+ // Should return false for routes with no GET methods
205
+ expectTypeOf<HasGetRoutes<typeof _noGetRoutes>>().toEqualTypeOf<false>();
206
+
207
+ // Should return false for empty routes
208
+ expectTypeOf<HasGetRoutes<typeof _emptyRoutes>>().toEqualTypeOf<false>();
209
+ });
210
+
211
+ test("Real-world usage scenarios", () => {
212
+ // Test with Chatno-like route configuration
213
+ const _chatnoLikeRoutes = [
214
+ defineRoute({
215
+ method: "GET",
216
+ path: "/home",
217
+ outputSchema: z.string(),
218
+ handler: async (_ctx, { json }) => json("Hello, world!"),
219
+ }),
220
+ defineRoute({
221
+ method: "GET",
222
+ path: "/thing/**:path",
223
+ outputSchema: z.string(),
224
+ handler: async (_ctx, { json }) => json("thing"),
225
+ }),
226
+ defineRoute({
227
+ method: "POST",
228
+ path: "/echo",
229
+ inputSchema: z.object({ number: z.number() }),
230
+ outputSchema: z.string(),
231
+ handler: async (_ctx, { json }) => json(""),
232
+ }),
233
+ defineRoute({
234
+ method: "GET",
235
+ path: "/ai-config",
236
+ outputSchema: z.object({
237
+ apiProvider: z.enum(["openai", "anthropic"]),
238
+ model: z.string(),
239
+ systemPrompt: z.string(),
240
+ }),
241
+ handler: async (_ctx, { json }) =>
242
+ json({
243
+ apiProvider: "openai" as const,
244
+ model: "gpt-4o",
245
+ systemPrompt: "",
246
+ }),
247
+ }),
248
+ ] as const;
249
+
250
+ // Should extract only GET paths
251
+ type ChatnoGetPaths = ExtractGetRoutePaths<typeof _chatnoLikeRoutes>;
252
+ expectTypeOf<ChatnoGetPaths>().toEqualTypeOf<"/home" | "/thing/**:path" | "/ai-config">();
253
+
254
+ // Should validate paths correctly
255
+ expectTypeOf<IsValidGetRoutePath<typeof _chatnoLikeRoutes, "/ai-config">>().toEqualTypeOf<true>();
256
+ expectTypeOf<IsValidGetRoutePath<typeof _chatnoLikeRoutes, "/echo">>().toEqualTypeOf<false>(); // POST route
257
+ });
258
+
259
+ test("Edge cases and error handling", () => {
260
+ // Routes with complex path patterns
261
+ const _complexRoutes = [
262
+ defineRoute({
263
+ method: "GET",
264
+ path: "/api/v1/users/:userId/posts/:postId/comments/:commentId",
265
+ outputSchema: z.object({ id: z.number(), content: z.string() }),
266
+ handler: async (_ctx, { json }) => json({ id: 1, content: "" }),
267
+ }),
268
+ defineRoute({
269
+ method: "GET",
270
+ path: "/files/**:filepath",
271
+ handler: async (_ctx, { json }) => json({}),
272
+ }),
273
+ defineRoute({
274
+ method: "GET",
275
+ path: "/admin/:section/:action",
276
+ outputSchema: z.object({ success: z.boolean() }),
277
+ handler: async (_ctx, { json }) => json({ success: true }),
278
+ }),
279
+ ] as const;
280
+
281
+ type ComplexPaths = ExtractGetRoutePaths<typeof _complexRoutes>;
282
+ expectTypeOf<ComplexPaths>().toEqualTypeOf<
283
+ | "/api/v1/users/:userId/posts/:postId/comments/:commentId"
284
+ | "/files/**:filepath"
285
+ | "/admin/:section/:action"
286
+ >();
287
+
288
+ // Should handle very long path names
289
+ expectTypeOf<
290
+ IsValidGetRoutePath<
291
+ typeof _complexRoutes,
292
+ "/api/v1/users/:userId/posts/:postId/comments/:commentId"
293
+ >
294
+ >().toEqualTypeOf<true>();
295
+
296
+ // Should handle wildcard paths
297
+ expectTypeOf<
298
+ IsValidGetRoutePath<typeof _complexRoutes, "/files/**:filepath">
299
+ >().toEqualTypeOf<true>();
300
+ });
301
+
302
+ test("Type constraint validation", () => {
303
+ // These tests ensure the types work correctly with const assertions and readonly arrays
304
+ const _routesWithoutConst = [
305
+ {
306
+ method: "GET" as const,
307
+ path: "/test" as const,
308
+ handler: async (_ctx: unknown, { empty }: { empty: () => Response }) => empty(),
309
+ },
310
+ ];
311
+
312
+ // Should work with non-const arrays too
313
+ type NonConstPaths = ExtractGetRoutePaths<typeof _routesWithoutConst>;
314
+ expectTypeOf<NonConstPaths>().toEqualTypeOf<"/test">();
315
+
316
+ // Should maintain type safety with const assertions
317
+ const _constRoutes = [
318
+ defineRoute({
319
+ method: "GET",
320
+ path: "/const-test",
321
+ handler: async (_ctx, { json }) => json({}),
322
+ }),
323
+ ] as const;
324
+
325
+ type ConstPaths = ExtractGetRoutePaths<typeof _constRoutes>;
326
+ expectTypeOf<ConstPaths>().toEqualTypeOf<"/const-test">();
327
+ });
328
+
329
+ test("GET route with outputSchema", () => {
330
+ // These tests ensure the types work correctly with const assertions and readonly arrays
331
+ const _routes = [
332
+ defineRoute({
333
+ method: "GET" as const,
334
+ path: "/test",
335
+ outputSchema: z.object({
336
+ name: z.string(),
337
+ }),
338
+ handler: async (_ctx, { json }) => json({ name: "test" }),
339
+ }),
340
+ ] as const;
341
+
342
+ type ConstPaths = ExtractGetRoutePaths<typeof _routes>;
343
+ expectTypeOf<ConstPaths>().toEqualTypeOf<"/test">();
344
+ });
345
+
346
+ describe("ExtractRouteByPath", () => {
347
+ const _fragmentConfig = {
348
+ name: "test-fragment",
349
+ routes: [
350
+ defineRoute({
351
+ method: "POST",
352
+ path: "/users",
353
+ inputSchema: z.object({ name: z.string(), email: z.string() }),
354
+ outputSchema: z.object({ id: z.number(), name: z.string(), email: z.string() }),
355
+ handler: async (_ctx, { json }) => json({ id: 1, name: "", email: "" }),
356
+ }),
357
+ defineRoute({
358
+ method: "PUT",
359
+ path: "/users/:id",
360
+ inputSchema: z.object({ name: z.string() }),
361
+ outputSchema: z.object({ id: z.number(), name: z.string() }),
362
+ handler: async ({ pathParams }, { json }) =>
363
+ json({ id: Number(pathParams["id"]), name: "" }),
364
+ }),
365
+ defineRoute({
366
+ method: "DELETE",
367
+ path: "/users/:id",
368
+ inputSchema: z.object({}),
369
+ outputSchema: z.object({ success: z.boolean() }),
370
+ handler: async (_ctx, { json }) => json({ success: true }),
371
+ }),
372
+ ],
373
+ } as const;
374
+
375
+ test("basic", () => {
376
+ type UsersRoute = ExtractRouteByPath<typeof _fragmentConfig.routes, "/users">;
377
+
378
+ expectTypeOf<UsersRoute>().toEqualTypeOf<
379
+ FragnoRouteConfig<
380
+ "POST",
381
+ "/users",
382
+ z.ZodObject<{
383
+ name: z.ZodString;
384
+ email: z.ZodString;
385
+ }>,
386
+ z.ZodObject<{
387
+ id: z.ZodNumber;
388
+ name: z.ZodString;
389
+ email: z.ZodString;
390
+ }>,
391
+ string,
392
+ string
393
+ >
394
+ >();
395
+ });
396
+ });
397
+
398
+ // FIXME: These are not great now
399
+ describe("FragnoClientMutatorData", () => {
400
+ type ZodObjectIdName = z.ZodObject<{
401
+ id: z.ZodNumber;
402
+ name: z.ZodString;
403
+ }>;
404
+
405
+ test("No inputSchema or outputSchema", () => {
406
+ type _Mutator1 = FragnoClientMutatorData<
407
+ "DELETE",
408
+ "/users/:id",
409
+ undefined,
410
+ undefined,
411
+ string,
412
+ string
413
+ >;
414
+ type MutateQuery = _Mutator1["mutateQuery"];
415
+
416
+ expectTypeOf<MutateQuery>().toEqualTypeOf<
417
+ (args?: {
418
+ body?: undefined;
419
+ path?: Record<"id", string> | undefined;
420
+ query?: Record<string, string>;
421
+ }) => Promise<undefined>
422
+ >();
423
+ });
424
+
425
+ test("No inputSchema", () => {
426
+ type _Mutator2 = FragnoClientMutatorData<
427
+ "DELETE",
428
+ "/users/:id",
429
+ undefined,
430
+ ZodObjectIdName,
431
+ string,
432
+ string
433
+ >;
434
+ type MutateQuery = _Mutator2["mutateQuery"];
435
+
436
+ expectTypeOf<MutateQuery>().toEqualTypeOf<
437
+ (args?: {
438
+ body?: undefined;
439
+ path?: Record<"id", string> | undefined;
440
+ query?: Record<string, string>;
441
+ }) => Promise<{
442
+ id: number;
443
+ name: string;
444
+ }>
445
+ >();
446
+ });
447
+
448
+ test("No outputSchema", () => {
449
+ type _Mutator3 = FragnoClientMutatorData<
450
+ "PUT",
451
+ "/users/:id",
452
+ ZodObjectIdName,
453
+ undefined,
454
+ string,
455
+ string
456
+ >;
457
+ type MutateQuery = _Mutator3["mutateQuery"];
458
+
459
+ expectTypeOf<MutateQuery>().toEqualTypeOf<
460
+ (args?: {
461
+ body?:
462
+ | {
463
+ id: number;
464
+ name: string;
465
+ }
466
+ | undefined;
467
+ path?: Record<"id", string> | undefined;
468
+ query?: Record<string, string>;
469
+ }) => Promise<undefined>
470
+ >();
471
+ });
472
+
473
+ test("With query parameters", () => {
474
+ type _Mutator4 = FragnoClientMutatorData<
475
+ "DELETE",
476
+ "/users/:id",
477
+ undefined,
478
+ undefined,
479
+ string,
480
+ "id" | "name"
481
+ >;
482
+
483
+ type MutateQuery = _Mutator4["mutateQuery"];
484
+
485
+ expectTypeOf<MutateQuery>().toEqualTypeOf<
486
+ (args?: {
487
+ body?: undefined;
488
+ path?: Record<"id", string> | undefined;
489
+ query?: Record<"id" | "name", string>;
490
+ }) => Promise<undefined>
491
+ >();
492
+ });
493
+ });
@@ -0,0 +1,173 @@
1
+ /**
2
+ * Our tests run with happy-dom by default, this defines `window` making it so that we `fetch()`
3
+ * instead of calling handlers directly. In this file we override it to be "node", so that `window`
4
+ * is not defined.
5
+ *
6
+ * @vitest-environment node
7
+ */
8
+
9
+ import { describe, expect, test } from "vitest";
10
+ import { type FragnoPublicClientConfig } from "../mod";
11
+ import { createClientBuilder } from "./client";
12
+ import { defineRoute } from "../api/route";
13
+ import { defineFragment } from "../api/fragment";
14
+ import { z } from "zod";
15
+ import { createAsyncIteratorFromCallback, waitForAsyncIterator } from "../util/async";
16
+
17
+ describe("server side rendering", () => {
18
+ const testFragmentDefinition = defineFragment("test-fragment");
19
+ const testRoutes = [
20
+ defineRoute({
21
+ method: "GET",
22
+ path: "/users",
23
+ outputSchema: z.array(z.object({ id: z.number(), name: z.string() })),
24
+ handler: async (_ctx, { json }) => json([{ id: 1, name: "John" }]),
25
+ }),
26
+ ] as const;
27
+
28
+ const clientConfig: FragnoPublicClientConfig = {
29
+ baseUrl: "http://localhost:3000",
30
+ };
31
+
32
+ describe("pre-conditions", () => {
33
+ test("Make sure window is undefined", () => {
34
+ expect(typeof window).toBe("undefined");
35
+ });
36
+ });
37
+
38
+ test("should call the handler directly", async () => {
39
+ const client = createClientBuilder(testFragmentDefinition, clientConfig, testRoutes);
40
+ const clientObj = {
41
+ useUsers: client.createHook("/users"),
42
+ };
43
+
44
+ const { useUsers } = clientObj;
45
+
46
+ const data = await useUsers.query({});
47
+
48
+ expect(data).toEqual([{ id: 1, name: "John" }]);
49
+ });
50
+
51
+ test("should be able to use the store server side", async () => {
52
+ const client = createClientBuilder(testFragmentDefinition, clientConfig, testRoutes);
53
+ const clientObj = {
54
+ useUsers: client.createHook("/users"),
55
+ };
56
+
57
+ const { useUsers } = clientObj;
58
+ const userStore = useUsers.store({});
59
+
60
+ const result = await waitForAsyncIterator(
61
+ createAsyncIteratorFromCallback(userStore.listen),
62
+ (value) => value.data !== undefined,
63
+ );
64
+
65
+ expect(result.data).toEqual([{ id: 1, name: "John" }]);
66
+ });
67
+
68
+ describe("mutation streaming", () => {
69
+ test("should support streaming responses for mutations", async () => {
70
+ const mutationStreamFragmentDefinition = defineFragment("mutation-stream-fragment");
71
+ const mutationStreamRoutes = [
72
+ defineRoute({
73
+ method: "POST",
74
+ path: "/process-items",
75
+ inputSchema: z.object({ items: z.array(z.string()) }),
76
+ outputSchema: z.array(z.object({ item: z.string(), status: z.string() })),
77
+ handler: async ({ input }, { jsonStream }) => {
78
+ const data = await input.valid();
79
+ const { items } = data!;
80
+ return jsonStream(async (stream) => {
81
+ for (const item of items) {
82
+ await stream.write({ item, status: "processed" });
83
+ await stream.sleep(1);
84
+ }
85
+ });
86
+ },
87
+ }),
88
+ ] as const;
89
+
90
+ const client = createClientBuilder(
91
+ mutationStreamFragmentDefinition,
92
+ clientConfig,
93
+ mutationStreamRoutes,
94
+ );
95
+ const mutator = client.createMutator("POST", "/process-items");
96
+
97
+ const result = await mutator.mutateQuery({
98
+ body: { items: ["item1", "item2", "item3"] },
99
+ });
100
+
101
+ // Streaming mutations return only the first item
102
+ expect(result).toEqual([
103
+ { item: "item1", status: "processed" },
104
+ { item: "item2", status: "processed" },
105
+ { item: "item3", status: "processed" },
106
+ ]);
107
+ });
108
+
109
+ test("should immediately return empty array for streaming mutations", async () => {
110
+ const mutationStreamFragmentDefinition = defineFragment("mutation-stream-fragment");
111
+ const mutationStreamRoutes = [
112
+ defineRoute({
113
+ method: "PUT",
114
+ path: "/update-batch",
115
+ inputSchema: z.object({ ids: z.array(z.number()) }),
116
+ outputSchema: z.array(z.object({ id: z.number(), updated: z.boolean() })),
117
+ handler: async (ctx, { jsonStream }) => {
118
+ const data = await ctx.input?.valid();
119
+ const { ids } = data!;
120
+ return jsonStream(async (stream) => {
121
+ for (const id of ids) {
122
+ await stream.write({ id, updated: true });
123
+ await stream.sleep(1);
124
+ }
125
+ });
126
+ },
127
+ }),
128
+ ] as const;
129
+
130
+ const client = createClientBuilder(
131
+ mutationStreamFragmentDefinition,
132
+ clientConfig,
133
+ mutationStreamRoutes,
134
+ );
135
+ const mutator = client.createMutator("PUT", "/update-batch");
136
+
137
+ const result = await mutator.mutatorStore.mutate({ body: { ids: [1, 2, 3] } });
138
+ // empty array
139
+ expect(result).toEqual([]);
140
+ });
141
+
142
+ test("should handle empty streaming response for mutations", async () => {
143
+ const mutationStreamFragmentDefinition = defineFragment("mutation-stream-fragment");
144
+ const mutationStreamRoutes = [
145
+ defineRoute({
146
+ method: "DELETE",
147
+ path: "/delete-items",
148
+ inputSchema: z.object({ ids: z.array(z.number()) }),
149
+ outputSchema: z.array(z.object({ id: z.number(), deleted: z.boolean() })),
150
+ handler: async (_ctx, { jsonStream }) => {
151
+ return jsonStream(async (_stream) => {
152
+ // Don't write anything
153
+ });
154
+ },
155
+ }),
156
+ ] as const;
157
+
158
+ const client = createClientBuilder(
159
+ mutationStreamFragmentDefinition,
160
+ clientConfig,
161
+ mutationStreamRoutes,
162
+ );
163
+ const mutator = client.createMutator("DELETE", "/delete-items");
164
+
165
+ // Empty streaming response should throw an error
166
+ await expect(
167
+ mutator.mutateQuery({
168
+ body: { ids: [1, 2, 3] },
169
+ }),
170
+ ).rejects.toThrow("NDJSON stream contained no valid items");
171
+ });
172
+ });
173
+ });