@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.
- package/.turbo/turbo-build.log +61 -0
- package/.turbo/turbo-types$colon$check.log +2 -0
- package/dist/api/api.d.ts +2 -0
- package/dist/api/api.js +3 -0
- package/dist/api-CBDGZiLC.d.ts +278 -0
- package/dist/api-CBDGZiLC.d.ts.map +1 -0
- package/dist/api-DgHfYjq2.js +54 -0
- package/dist/api-DgHfYjq2.js.map +1 -0
- package/dist/client/client.d.ts +3 -0
- package/dist/client/client.js +6 -0
- package/dist/client/client.svelte.d.ts +33 -0
- package/dist/client/client.svelte.d.ts.map +1 -0
- package/dist/client/client.svelte.js +123 -0
- package/dist/client/client.svelte.js.map +1 -0
- package/dist/client/react.d.ts +58 -0
- package/dist/client/react.d.ts.map +1 -0
- package/dist/client/react.js +80 -0
- package/dist/client/react.js.map +1 -0
- package/dist/client/vanilla.d.ts +61 -0
- package/dist/client/vanilla.d.ts.map +1 -0
- package/dist/client/vanilla.js +136 -0
- package/dist/client/vanilla.js.map +1 -0
- package/dist/client/vue.d.ts +39 -0
- package/dist/client/vue.d.ts.map +1 -0
- package/dist/client/vue.js +108 -0
- package/dist/client/vue.js.map +1 -0
- package/dist/client-DWjxKDnE.js +703 -0
- package/dist/client-DWjxKDnE.js.map +1 -0
- package/dist/client-XFdAy-IQ.d.ts +287 -0
- package/dist/client-XFdAy-IQ.d.ts.map +1 -0
- package/dist/integrations/astro.d.ts +18 -0
- package/dist/integrations/astro.d.ts.map +1 -0
- package/dist/integrations/astro.js +16 -0
- package/dist/integrations/astro.js.map +1 -0
- package/dist/integrations/next-js.d.ts +15 -0
- package/dist/integrations/next-js.d.ts.map +1 -0
- package/dist/integrations/next-js.js +17 -0
- package/dist/integrations/next-js.js.map +1 -0
- package/dist/integrations/react-ssr.d.ts +19 -0
- package/dist/integrations/react-ssr.d.ts.map +1 -0
- package/dist/integrations/react-ssr.js +38 -0
- package/dist/integrations/react-ssr.js.map +1 -0
- package/dist/integrations/svelte-kit.d.ts +21 -0
- package/dist/integrations/svelte-kit.d.ts.map +1 -0
- package/dist/integrations/svelte-kit.js +18 -0
- package/dist/integrations/svelte-kit.js.map +1 -0
- package/dist/mod.d.ts +3 -0
- package/dist/mod.js +177 -0
- package/dist/mod.js.map +1 -0
- package/dist/route-Bp6eByhz.js +331 -0
- package/dist/route-Bp6eByhz.js.map +1 -0
- package/dist/ssr-tJHqcNSw.js +48 -0
- package/dist/ssr-tJHqcNSw.js.map +1 -0
- package/package.json +127 -0
- package/src/api/api.test.ts +140 -0
- package/src/api/api.ts +106 -0
- package/src/api/error.ts +47 -0
- package/src/api/fragment.test.ts +509 -0
- package/src/api/fragment.ts +277 -0
- package/src/api/internal/path-runtime.test.ts +121 -0
- package/src/api/internal/path-type.test.ts +602 -0
- package/src/api/internal/path.ts +322 -0
- package/src/api/internal/response-stream.ts +118 -0
- package/src/api/internal/route.test.ts +56 -0
- package/src/api/internal/route.ts +9 -0
- package/src/api/request-input-context.test.ts +437 -0
- package/src/api/request-input-context.ts +201 -0
- package/src/api/request-middleware.test.ts +544 -0
- package/src/api/request-middleware.ts +126 -0
- package/src/api/request-output-context.test.ts +626 -0
- package/src/api/request-output-context.ts +175 -0
- package/src/api/route.test.ts +176 -0
- package/src/api/route.ts +152 -0
- package/src/client/client-builder.test.ts +264 -0
- package/src/client/client-error.test.ts +15 -0
- package/src/client/client-error.ts +141 -0
- package/src/client/client-types.test.ts +493 -0
- package/src/client/client.ssr.test.ts +173 -0
- package/src/client/client.svelte.test.ts +837 -0
- package/src/client/client.svelte.ts +278 -0
- package/src/client/client.test.ts +1690 -0
- package/src/client/client.ts +1035 -0
- package/src/client/component.test.svelte +21 -0
- package/src/client/internal/ndjson-streaming.test.ts +457 -0
- package/src/client/internal/ndjson-streaming.ts +248 -0
- package/src/client/react.test.ts +947 -0
- package/src/client/react.ts +241 -0
- package/src/client/vanilla.test.ts +867 -0
- package/src/client/vanilla.ts +265 -0
- package/src/client/vue.test.ts +754 -0
- package/src/client/vue.ts +242 -0
- package/src/http/http-status.ts +60 -0
- package/src/integrations/astro.ts +17 -0
- package/src/integrations/next-js.ts +31 -0
- package/src/integrations/react-ssr.ts +40 -0
- package/src/integrations/svelte-kit.ts +41 -0
- package/src/mod.ts +20 -0
- package/src/util/async.test.ts +85 -0
- package/src/util/async.ts +96 -0
- package/src/util/content-type.test.ts +136 -0
- package/src/util/content-type.ts +84 -0
- package/src/util/nanostores.test.ts +28 -0
- package/src/util/nanostores.ts +65 -0
- package/src/util/ssr.ts +75 -0
- package/src/util/types-util.ts +16 -0
- package/tsconfig.json +10 -0
- package/tsdown.config.ts +21 -0
- 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
|
+
});
|