@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,754 @@
|
|
|
1
|
+
import { test, expect, describe, vi, beforeEach, afterEach, assert } from "vitest";
|
|
2
|
+
import { type FragnoPublicClientConfig } from "../mod";
|
|
3
|
+
import { createClientBuilder } from "./client";
|
|
4
|
+
import { defineRoute } from "../api/route";
|
|
5
|
+
import { defineFragment } from "../api/fragment";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { refToAtom, useFragno } from "./vue";
|
|
8
|
+
import { waitFor } from "@testing-library/vue";
|
|
9
|
+
import { nextTick, ref, watch } from "vue";
|
|
10
|
+
import { FragnoClientUnknownApiError } from "./client-error";
|
|
11
|
+
import { atom } from "nanostores";
|
|
12
|
+
|
|
13
|
+
global.fetch = vi.fn();
|
|
14
|
+
|
|
15
|
+
async function sleep(ms: number) {
|
|
16
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe("refToAtom", () => {
|
|
20
|
+
test("should create an atom from a ref", () => {
|
|
21
|
+
const r = ref(123);
|
|
22
|
+
const a = refToAtom(r);
|
|
23
|
+
expect(a.get()).toBe(123);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("should update the atom when the ref changes", async () => {
|
|
27
|
+
const r = ref(123);
|
|
28
|
+
const a = refToAtom(r);
|
|
29
|
+
expect(a.get()).toBe(123);
|
|
30
|
+
r.value = 456;
|
|
31
|
+
await sleep(0);
|
|
32
|
+
expect(a.get()).toBe(456);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("ref nested in object", async () => {
|
|
36
|
+
const obj = ref({ a: 1, nested: { b: 2 } });
|
|
37
|
+
|
|
38
|
+
let listenExecuted = false;
|
|
39
|
+
watch(
|
|
40
|
+
obj,
|
|
41
|
+
(newObj) => {
|
|
42
|
+
expect(newObj.a).toBe(3);
|
|
43
|
+
expect(newObj.nested.b).toBe(5);
|
|
44
|
+
listenExecuted = true;
|
|
45
|
+
},
|
|
46
|
+
{ deep: true },
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
obj.value.a = 3;
|
|
50
|
+
obj.value.nested.b = 5;
|
|
51
|
+
|
|
52
|
+
await nextTick();
|
|
53
|
+
expect(listenExecuted).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe("createVueHook", () => {
|
|
58
|
+
const clientConfig: FragnoPublicClientConfig = {
|
|
59
|
+
baseUrl: "http://localhost:3000",
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
beforeEach(() => {
|
|
63
|
+
vi.clearAllMocks();
|
|
64
|
+
(global.fetch as ReturnType<typeof vi.fn>).mockReset();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
afterEach(() => {
|
|
68
|
+
vi.restoreAllMocks();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("Hook should function", async () => {
|
|
72
|
+
const testFragmentDefinition = defineFragment("test-fragment");
|
|
73
|
+
const testRoutes = [
|
|
74
|
+
defineRoute({
|
|
75
|
+
method: "GET",
|
|
76
|
+
path: "/users",
|
|
77
|
+
outputSchema: z.array(z.object({ id: z.number(), name: z.string() })),
|
|
78
|
+
handler: async (_ctx, { json }) => json([{ id: 1, name: "John" }]),
|
|
79
|
+
}),
|
|
80
|
+
] as const;
|
|
81
|
+
|
|
82
|
+
vi.mocked(global.fetch).mockImplementationOnce(
|
|
83
|
+
async () =>
|
|
84
|
+
({
|
|
85
|
+
headers: new Headers(),
|
|
86
|
+
ok: true,
|
|
87
|
+
json: async () => [{ id: 1, name: "John" }],
|
|
88
|
+
}) as Response,
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const client = createClientBuilder(testFragmentDefinition, clientConfig, testRoutes);
|
|
92
|
+
const clientObj = {
|
|
93
|
+
useUsers: client.createHook("/users"),
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const { useUsers } = useFragno(clientObj);
|
|
97
|
+
const { loading, data, error } = useUsers();
|
|
98
|
+
|
|
99
|
+
await waitFor(() => {
|
|
100
|
+
expect(loading.value).toBe(false);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
expect(data.value).toEqual([{ id: 1, name: "John" }]);
|
|
104
|
+
expect(error.value).toBeUndefined();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("Should support path parameters and update reactively when Vue ref changes", async () => {
|
|
108
|
+
const testFragmentDefinition = defineFragment("test-fragment");
|
|
109
|
+
const testRoutes = [
|
|
110
|
+
defineRoute({
|
|
111
|
+
method: "GET",
|
|
112
|
+
path: "/users/:id",
|
|
113
|
+
outputSchema: z.object({ id: z.number(), name: z.string() }),
|
|
114
|
+
handler: async ({ pathParams }, { json }) =>
|
|
115
|
+
json({ id: Number(pathParams["id"]), name: "John" }),
|
|
116
|
+
}),
|
|
117
|
+
] as const;
|
|
118
|
+
|
|
119
|
+
// Mock fetch to extract the user ID from the URL and return a user object with that ID.
|
|
120
|
+
vi.mocked(global.fetch).mockImplementation(async (input) => {
|
|
121
|
+
assert(typeof input === "string");
|
|
122
|
+
|
|
123
|
+
// Regex to extract id value from a URL string, matching only on /users/:id
|
|
124
|
+
const [, id] = String(input).match(/\/users\/([^/]+)/) ?? [];
|
|
125
|
+
|
|
126
|
+
expect(id).toBeDefined();
|
|
127
|
+
expect(+id).not.toBeNaN();
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
headers: new Headers(),
|
|
131
|
+
ok: true,
|
|
132
|
+
json: async () => ({ id: Number(id), name: "John" }),
|
|
133
|
+
} as Response;
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const client = createClientBuilder(testFragmentDefinition, clientConfig, testRoutes);
|
|
137
|
+
const clientObj = {
|
|
138
|
+
useUser: client.createHook("/users/:id"),
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const id = ref("123");
|
|
142
|
+
|
|
143
|
+
const { useUser } = useFragno(clientObj);
|
|
144
|
+
const { loading, data, error } = useUser({ path: { id } });
|
|
145
|
+
|
|
146
|
+
await waitFor(() => {
|
|
147
|
+
expect(loading.value).toBe(false);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
expect(data.value).toEqual({ id: 123, name: "John" });
|
|
151
|
+
expect(error.value).toBeUndefined();
|
|
152
|
+
|
|
153
|
+
// Update the id value
|
|
154
|
+
id.value = "456";
|
|
155
|
+
|
|
156
|
+
await waitFor(() => {
|
|
157
|
+
expect(data.value).toEqual({ id: 456, name: "John" });
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
expect(fetch).toHaveBeenCalledTimes(2);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("Should support path parameters and update reactively when Nanostores Atom changes", async () => {
|
|
164
|
+
const testFragmentDefinition = defineFragment("test-fragment");
|
|
165
|
+
const testRoutes = [
|
|
166
|
+
defineRoute({
|
|
167
|
+
method: "GET",
|
|
168
|
+
path: "/users/:id",
|
|
169
|
+
outputSchema: z.object({ id: z.number(), name: z.string() }),
|
|
170
|
+
handler: async ({ pathParams }, { json }) =>
|
|
171
|
+
json({ id: Number(pathParams["id"]), name: "John" }),
|
|
172
|
+
}),
|
|
173
|
+
] as const;
|
|
174
|
+
|
|
175
|
+
// Mock fetch to extract the user ID from the URL and return a user object with that ID.
|
|
176
|
+
vi.mocked(global.fetch).mockImplementation(async (input) => {
|
|
177
|
+
assert(typeof input === "string");
|
|
178
|
+
|
|
179
|
+
// Regex to extract id value from a URL string, matching only on /users/:id
|
|
180
|
+
const [, id] = String(input).match(/\/users\/([^/]+)/) ?? [];
|
|
181
|
+
|
|
182
|
+
expect(id).toBeDefined();
|
|
183
|
+
expect(+id).not.toBeNaN();
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
headers: new Headers(),
|
|
187
|
+
ok: true,
|
|
188
|
+
json: async () => ({ id: Number(id), name: "John" }),
|
|
189
|
+
} as Response;
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
const client = createClientBuilder(testFragmentDefinition, clientConfig, testRoutes);
|
|
193
|
+
const clientObj = {
|
|
194
|
+
useUser: client.createHook("/users/:id"),
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const id = atom("123");
|
|
198
|
+
|
|
199
|
+
const { useUser } = useFragno(clientObj);
|
|
200
|
+
const { loading, data, error } = useUser({ path: { id } });
|
|
201
|
+
|
|
202
|
+
await waitFor(() => {
|
|
203
|
+
expect(loading.value).toBe(false);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
expect(data.value).toEqual({ id: 123, name: "John" });
|
|
207
|
+
expect(error.value).toBeUndefined();
|
|
208
|
+
|
|
209
|
+
// Update the id value
|
|
210
|
+
id.set("456");
|
|
211
|
+
|
|
212
|
+
await waitFor(() => {
|
|
213
|
+
expect(data.value).toEqual({ id: 456, name: "John" });
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
expect(fetch).toHaveBeenCalledTimes(2);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test("Should handle errors gracefully", async () => {
|
|
220
|
+
const testFragmentDefinition = defineFragment("test-fragment");
|
|
221
|
+
const testRoutes = [
|
|
222
|
+
defineRoute({
|
|
223
|
+
method: "GET",
|
|
224
|
+
path: "/users",
|
|
225
|
+
outputSchema: z.array(z.object({ id: z.number(), name: z.string() })),
|
|
226
|
+
handler: async (_ctx, { json }) => json([{ id: 1, name: "John" }]),
|
|
227
|
+
}),
|
|
228
|
+
] as const;
|
|
229
|
+
|
|
230
|
+
vi.mocked(global.fetch).mockImplementationOnce(
|
|
231
|
+
async () =>
|
|
232
|
+
({
|
|
233
|
+
headers: new Headers(),
|
|
234
|
+
ok: false,
|
|
235
|
+
status: 500,
|
|
236
|
+
statusText: "Internal Server Error",
|
|
237
|
+
json: async () => ({ message: "Server error" }),
|
|
238
|
+
}) as Response,
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
const client = createClientBuilder(testFragmentDefinition, clientConfig, testRoutes);
|
|
242
|
+
const clientObj = {
|
|
243
|
+
useUsers: client.createHook("/users"),
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
const { useUsers } = useFragno(clientObj);
|
|
247
|
+
const { loading, data, error } = useUsers();
|
|
248
|
+
|
|
249
|
+
await waitFor(() => {
|
|
250
|
+
expect(loading.value).toBe(false);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
expect(data.value).toBeUndefined();
|
|
254
|
+
expect(error.value).toBeDefined();
|
|
255
|
+
expect(error.value).toBeInstanceOf(FragnoClientUnknownApiError);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test("Should track loading states correctly", async () => {
|
|
259
|
+
const testFragmentDefinition = defineFragment("test-fragment");
|
|
260
|
+
const testRoutes = [
|
|
261
|
+
defineRoute({
|
|
262
|
+
method: "GET",
|
|
263
|
+
path: "/users",
|
|
264
|
+
outputSchema: z.array(z.object({ id: z.number(), name: z.string() })),
|
|
265
|
+
handler: async (_ctx, { json }) => json([{ id: 1, name: "John" }]),
|
|
266
|
+
}),
|
|
267
|
+
] as const;
|
|
268
|
+
|
|
269
|
+
let resolvePromise: (value: Response) => void;
|
|
270
|
+
const fetchPromise = new Promise<Response>((resolve) => {
|
|
271
|
+
resolvePromise = resolve;
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
vi.mocked(global.fetch).mockImplementationOnce(() => fetchPromise);
|
|
275
|
+
|
|
276
|
+
const client = createClientBuilder(testFragmentDefinition, clientConfig, testRoutes);
|
|
277
|
+
const clientObj = {
|
|
278
|
+
useUsers: client.createHook("/users"),
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
const { useUsers } = useFragno(clientObj);
|
|
282
|
+
const { loading, data, error } = useUsers();
|
|
283
|
+
|
|
284
|
+
// Initially loading should be true
|
|
285
|
+
expect(loading.value).toBe(true);
|
|
286
|
+
expect(data.value).toBeUndefined();
|
|
287
|
+
expect(error.value).toBeUndefined();
|
|
288
|
+
|
|
289
|
+
// Resolve the fetch
|
|
290
|
+
resolvePromise!({
|
|
291
|
+
headers: new Headers(),
|
|
292
|
+
ok: true,
|
|
293
|
+
json: async () => [{ id: 1, name: "John" }],
|
|
294
|
+
} as Response);
|
|
295
|
+
|
|
296
|
+
await waitFor(() => {
|
|
297
|
+
expect(loading.value).toBe(false);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
expect(data.value).toEqual([{ id: 1, name: "John" }]);
|
|
301
|
+
expect(error.value).toBeUndefined();
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
test("Should handle mutator hooks", async () => {
|
|
305
|
+
const testFragmentDefinition = defineFragment("test-fragment");
|
|
306
|
+
const testRoutes = [
|
|
307
|
+
defineRoute({
|
|
308
|
+
method: "POST",
|
|
309
|
+
path: "/users",
|
|
310
|
+
inputSchema: z.object({ name: z.string(), email: z.string() }),
|
|
311
|
+
outputSchema: z.object({ id: z.number(), name: z.string(), email: z.string() }),
|
|
312
|
+
handler: async ({ input }, { json }) => {
|
|
313
|
+
const { name, email } = await input.valid();
|
|
314
|
+
return json({ id: 1, name, email });
|
|
315
|
+
},
|
|
316
|
+
}),
|
|
317
|
+
] as const;
|
|
318
|
+
|
|
319
|
+
vi.mocked(global.fetch).mockImplementationOnce(
|
|
320
|
+
async () =>
|
|
321
|
+
({
|
|
322
|
+
headers: new Headers(),
|
|
323
|
+
ok: true,
|
|
324
|
+
json: async () => ({ id: 1, name: "John Doe", email: "john@example.com" }),
|
|
325
|
+
}) as Response,
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
const client = createClientBuilder(testFragmentDefinition, clientConfig, testRoutes);
|
|
329
|
+
const clientObj = {
|
|
330
|
+
createUser: client.createMutator("POST", "/users"),
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
const { createUser } = useFragno(clientObj);
|
|
334
|
+
const { mutate, loading, data, error } = createUser();
|
|
335
|
+
|
|
336
|
+
expect(loading.value).toBe(false);
|
|
337
|
+
expect(data.value).toBeUndefined();
|
|
338
|
+
expect(error.value).toBeUndefined();
|
|
339
|
+
|
|
340
|
+
const result = await mutate({
|
|
341
|
+
body: { name: "John Doe", email: "john@example.com" },
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
expect(result).toEqual({ id: 1, name: "John Doe", email: "john@example.com" });
|
|
345
|
+
expect(data.value).toEqual({ id: 1, name: "John Doe", email: "john@example.com" });
|
|
346
|
+
expect(error.value).toBeUndefined();
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
test("Should handle query parameters", async () => {
|
|
350
|
+
const testFragmentDefinition = defineFragment("test-fragment");
|
|
351
|
+
const testRoutes = [
|
|
352
|
+
defineRoute({
|
|
353
|
+
method: "GET",
|
|
354
|
+
path: "/users",
|
|
355
|
+
outputSchema: z.array(z.object({ id: z.number(), name: z.string() })),
|
|
356
|
+
handler: async (_ctx, { json }) => json([{ id: 1, name: "John" }]),
|
|
357
|
+
}),
|
|
358
|
+
] as const;
|
|
359
|
+
|
|
360
|
+
vi.mocked(global.fetch).mockImplementation(async (input) => {
|
|
361
|
+
assert(typeof input === "string");
|
|
362
|
+
expect(input).toContain("limit=10");
|
|
363
|
+
expect(input).toContain("page=1");
|
|
364
|
+
|
|
365
|
+
return {
|
|
366
|
+
headers: new Headers(),
|
|
367
|
+
ok: true,
|
|
368
|
+
json: async () => [{ id: 1, name: "John" }],
|
|
369
|
+
} as Response;
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
const client = createClientBuilder(testFragmentDefinition, clientConfig, testRoutes);
|
|
373
|
+
const clientObj = {
|
|
374
|
+
useUsers: client.createHook("/users"),
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
const limit = ref("10");
|
|
378
|
+
const page = ref("1");
|
|
379
|
+
|
|
380
|
+
const { useUsers } = useFragno(clientObj);
|
|
381
|
+
const { loading, data, error } = useUsers({ query: { limit, page } });
|
|
382
|
+
|
|
383
|
+
await waitFor(() => {
|
|
384
|
+
expect(loading.value).toBe(false);
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
expect(data.value).toEqual([{ id: 1, name: "John" }]);
|
|
388
|
+
expect(error.value).toBeUndefined();
|
|
389
|
+
|
|
390
|
+
// Update query params
|
|
391
|
+
limit.value = "20";
|
|
392
|
+
page.value = "2";
|
|
393
|
+
|
|
394
|
+
await waitFor(() => {
|
|
395
|
+
expect(fetch).toHaveBeenCalledTimes(2);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
// Verify the second call has updated params
|
|
399
|
+
const lastCall = vi.mocked(global.fetch).mock.calls[1];
|
|
400
|
+
expect(lastCall[0]).toContain("limit=20");
|
|
401
|
+
expect(lastCall[0]).toContain("page=2");
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
test("Should handle multiple hooks together", async () => {
|
|
405
|
+
const testFragmentDefinition = defineFragment("test-fragment");
|
|
406
|
+
const testRoutes = [
|
|
407
|
+
defineRoute({
|
|
408
|
+
method: "GET",
|
|
409
|
+
path: "/users",
|
|
410
|
+
outputSchema: z.array(z.object({ id: z.number(), name: z.string() })),
|
|
411
|
+
handler: async (_ctx, { json }) => json([{ id: 1, name: "John" }]),
|
|
412
|
+
}),
|
|
413
|
+
defineRoute({
|
|
414
|
+
method: "GET",
|
|
415
|
+
path: "/posts",
|
|
416
|
+
outputSchema: z.array(z.object({ id: z.number(), title: z.string() })),
|
|
417
|
+
handler: async (_ctx, { json }) => json([{ id: 1, title: "First Post" }]),
|
|
418
|
+
}),
|
|
419
|
+
defineRoute({
|
|
420
|
+
method: "DELETE",
|
|
421
|
+
path: "/users/:id",
|
|
422
|
+
inputSchema: z.object({}),
|
|
423
|
+
outputSchema: z.object({ success: z.boolean() }),
|
|
424
|
+
handler: async (_ctx, { json }) => json({ success: true }),
|
|
425
|
+
}),
|
|
426
|
+
] as const;
|
|
427
|
+
|
|
428
|
+
vi.mocked(global.fetch).mockImplementation(async (input) => {
|
|
429
|
+
assert(typeof input === "string");
|
|
430
|
+
|
|
431
|
+
if (input.includes("/users") && !input.includes("/users/")) {
|
|
432
|
+
return {
|
|
433
|
+
headers: new Headers(),
|
|
434
|
+
ok: true,
|
|
435
|
+
json: async () => [{ id: 1, name: "John" }],
|
|
436
|
+
} as Response;
|
|
437
|
+
} else if (input.includes("/posts")) {
|
|
438
|
+
return {
|
|
439
|
+
headers: new Headers(),
|
|
440
|
+
ok: true,
|
|
441
|
+
json: async () => [{ id: 1, title: "First Post" }],
|
|
442
|
+
} as Response;
|
|
443
|
+
} else if (input.includes("/users/")) {
|
|
444
|
+
return {
|
|
445
|
+
headers: new Headers(),
|
|
446
|
+
ok: true,
|
|
447
|
+
json: async () => ({ success: true }),
|
|
448
|
+
} as Response;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
throw new Error(`Unexpected URL: ${input}`);
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
const client = createClientBuilder(testFragmentDefinition, clientConfig, testRoutes);
|
|
455
|
+
const clientObj = {
|
|
456
|
+
useUsers: client.createHook("/users"),
|
|
457
|
+
usePosts: client.createHook("/posts"),
|
|
458
|
+
deleteUser: client.createMutator("DELETE", "/users/:id"),
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
const { useUsers, usePosts, deleteUser } = useFragno(clientObj);
|
|
462
|
+
|
|
463
|
+
// Use multiple hooks
|
|
464
|
+
const users = useUsers();
|
|
465
|
+
const posts = usePosts();
|
|
466
|
+
const deleter = deleteUser();
|
|
467
|
+
|
|
468
|
+
await waitFor(() => {
|
|
469
|
+
expect(users.loading.value).toBe(false);
|
|
470
|
+
expect(posts.loading.value).toBe(false);
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
expect(users.data.value).toEqual([{ id: 1, name: "John" }]);
|
|
474
|
+
expect(posts.data.value).toEqual([{ id: 1, title: "First Post" }]);
|
|
475
|
+
|
|
476
|
+
// Use the mutator
|
|
477
|
+
const result = await deleter.mutate({
|
|
478
|
+
body: {},
|
|
479
|
+
path: { id: "1" },
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
expect(result).toEqual({ success: true });
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
test("Should handle mixed reactive parameters - ref path param, atom and ref query params, with reactive updates", async () => {
|
|
486
|
+
const testFragmentDefinition = defineFragment("test-fragment");
|
|
487
|
+
const testRoutes = [
|
|
488
|
+
defineRoute({
|
|
489
|
+
method: "GET",
|
|
490
|
+
path: "/users/:id/posts",
|
|
491
|
+
inputSchema: z.object({
|
|
492
|
+
limit: z.string().optional(),
|
|
493
|
+
category: z.string().optional(),
|
|
494
|
+
sort: z.string().optional(),
|
|
495
|
+
}),
|
|
496
|
+
outputSchema: z.array(
|
|
497
|
+
z.object({ id: z.number(), title: z.string(), category: z.string() }),
|
|
498
|
+
),
|
|
499
|
+
handler: async (_ctx, { empty }) => empty(),
|
|
500
|
+
}),
|
|
501
|
+
] as const;
|
|
502
|
+
|
|
503
|
+
// Mock fetch to verify URL construction and parameter passing
|
|
504
|
+
vi.mocked(global.fetch).mockImplementation(async (input) => {
|
|
505
|
+
assert(typeof input === "string");
|
|
506
|
+
|
|
507
|
+
// Extract user ID from path
|
|
508
|
+
const [, userId] = String(input).match(/\/users\/([^/]+)\/posts/) ?? [];
|
|
509
|
+
expect(userId).toBeDefined();
|
|
510
|
+
expect(+userId).not.toBeNaN();
|
|
511
|
+
|
|
512
|
+
// Parse query parameters
|
|
513
|
+
const url = new URL(input);
|
|
514
|
+
const limit = url.searchParams.get("limit") || "5";
|
|
515
|
+
const category = url.searchParams.get("category") || "general";
|
|
516
|
+
const sort = url.searchParams.get("sort") || "asc";
|
|
517
|
+
|
|
518
|
+
return {
|
|
519
|
+
headers: new Headers(),
|
|
520
|
+
ok: true,
|
|
521
|
+
json: async () => [
|
|
522
|
+
{
|
|
523
|
+
id: Number(userId) * 100,
|
|
524
|
+
title: `Post for user ${userId}`,
|
|
525
|
+
category: `${category}-${limit}-${sort}`,
|
|
526
|
+
},
|
|
527
|
+
],
|
|
528
|
+
} as Response;
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
const client = createClientBuilder(testFragmentDefinition, clientConfig, testRoutes);
|
|
532
|
+
const clientObj = {
|
|
533
|
+
useUserPosts: client.createHook("/users/:id/posts"),
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
// Set up reactive parameters
|
|
537
|
+
const userId = ref("1"); // Vue ref for path parameter
|
|
538
|
+
const limit = atom("10"); // Nanostores atom for query parameter
|
|
539
|
+
const category = ref("tech"); // Vue ref for query parameter
|
|
540
|
+
const sort = "desc"; // Normal string for query parameter
|
|
541
|
+
|
|
542
|
+
const { useUserPosts } = useFragno(clientObj);
|
|
543
|
+
const { loading, data, error } = useUserPosts({
|
|
544
|
+
path: { id: userId },
|
|
545
|
+
query: { limit, category, sort },
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
// Wait for initial load
|
|
549
|
+
await waitFor(() => {
|
|
550
|
+
expect(loading.value).toBe(false);
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
expect(data.value).toEqual([{ id: 100, title: "Post for user 1", category: "tech-10-desc" }]);
|
|
554
|
+
expect(error.value).toBeUndefined();
|
|
555
|
+
expect(fetch).toHaveBeenCalledTimes(1);
|
|
556
|
+
|
|
557
|
+
// Verify initial URL construction
|
|
558
|
+
const firstCall = vi.mocked(global.fetch).mock.calls[0][0] as string;
|
|
559
|
+
expect(firstCall).toContain("/users/1/posts");
|
|
560
|
+
expect(firstCall).toContain("limit=10");
|
|
561
|
+
expect(firstCall).toContain("category=tech");
|
|
562
|
+
expect(firstCall).toContain("sort=desc");
|
|
563
|
+
|
|
564
|
+
// Update the Vue ref path parameter
|
|
565
|
+
userId.value = "2";
|
|
566
|
+
|
|
567
|
+
await waitFor(() => {
|
|
568
|
+
expect(data.value).toEqual([{ id: 200, title: "Post for user 2", category: "tech-10-desc" }]);
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
expect(fetch).toHaveBeenCalledTimes(2);
|
|
572
|
+
|
|
573
|
+
// Verify the second call has updated path param
|
|
574
|
+
const secondCall = vi.mocked(global.fetch).mock.calls[1][0] as string;
|
|
575
|
+
expect(secondCall).toContain("/users/2/posts");
|
|
576
|
+
expect(secondCall).toContain("limit=10");
|
|
577
|
+
expect(secondCall).toContain("category=tech");
|
|
578
|
+
expect(secondCall).toContain("sort=desc");
|
|
579
|
+
|
|
580
|
+
// Update the nanostores atom query parameter
|
|
581
|
+
limit.set("20");
|
|
582
|
+
|
|
583
|
+
await waitFor(() => {
|
|
584
|
+
expect(data.value).toEqual([{ id: 200, title: "Post for user 2", category: "tech-20-desc" }]);
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
expect(fetch).toHaveBeenCalledTimes(3);
|
|
588
|
+
|
|
589
|
+
// Verify the third call has updated atom query param
|
|
590
|
+
const thirdCall = vi.mocked(global.fetch).mock.calls[2][0] as string;
|
|
591
|
+
expect(thirdCall).toContain("/users/2/posts");
|
|
592
|
+
expect(thirdCall).toContain("limit=20");
|
|
593
|
+
expect(thirdCall).toContain("category=tech");
|
|
594
|
+
expect(thirdCall).toContain("sort=desc");
|
|
595
|
+
|
|
596
|
+
// Update the Vue ref query parameter
|
|
597
|
+
category.value = "science";
|
|
598
|
+
|
|
599
|
+
await waitFor(() => {
|
|
600
|
+
expect(data.value).toEqual([
|
|
601
|
+
{ id: 200, title: "Post for user 2", category: "science-20-desc" },
|
|
602
|
+
]);
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
expect(fetch).toHaveBeenCalledTimes(4);
|
|
606
|
+
|
|
607
|
+
// Verify the fourth call has updated ref query param
|
|
608
|
+
const fourthCall = vi.mocked(global.fetch).mock.calls[3][0] as string;
|
|
609
|
+
expect(fourthCall).toContain("/users/2/posts");
|
|
610
|
+
expect(fourthCall).toContain("limit=20");
|
|
611
|
+
expect(fourthCall).toContain("category=science");
|
|
612
|
+
expect(fourthCall).toContain("sort=desc");
|
|
613
|
+
|
|
614
|
+
// Update both reactive parameters simultaneously
|
|
615
|
+
userId.value = "3";
|
|
616
|
+
limit.set("5");
|
|
617
|
+
category.value = "news";
|
|
618
|
+
|
|
619
|
+
await waitFor(() => {
|
|
620
|
+
expect(data.value).toEqual([{ id: 300, title: "Post for user 3", category: "news-5-desc" }]);
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
expect(fetch).toHaveBeenCalledTimes(5);
|
|
624
|
+
|
|
625
|
+
// Verify the final call has all updated parameters
|
|
626
|
+
const finalCall = vi.mocked(global.fetch).mock.calls[4][0] as string;
|
|
627
|
+
expect(finalCall).toContain("/users/3/posts");
|
|
628
|
+
expect(finalCall).toContain("limit=5");
|
|
629
|
+
expect(finalCall).toContain("category=news");
|
|
630
|
+
expect(finalCall).toContain("sort=desc"); // Static parameter should remain unchanged
|
|
631
|
+
});
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
describe("createVueMutator", () => {
|
|
635
|
+
const clientConfig: FragnoPublicClientConfig = {
|
|
636
|
+
baseUrl: "http://localhost:3000",
|
|
637
|
+
};
|
|
638
|
+
|
|
639
|
+
beforeEach(() => {
|
|
640
|
+
vi.clearAllMocks();
|
|
641
|
+
(global.fetch as ReturnType<typeof vi.fn>).mockReset();
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
afterEach(() => {
|
|
645
|
+
vi.restoreAllMocks();
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
test("Should handle mutator with path parameters", async () => {
|
|
649
|
+
const testFragmentDefinition = defineFragment("test-fragment");
|
|
650
|
+
const testRoutes = [
|
|
651
|
+
defineRoute({
|
|
652
|
+
method: "PUT",
|
|
653
|
+
path: "/users/:id",
|
|
654
|
+
inputSchema: z.object({ name: z.string() }),
|
|
655
|
+
outputSchema: z.object({ id: z.number(), name: z.string() }),
|
|
656
|
+
handler: async ({ pathParams, input }, { json }) => {
|
|
657
|
+
const { name } = await input.valid();
|
|
658
|
+
return json({ id: Number(pathParams["id"]), name });
|
|
659
|
+
},
|
|
660
|
+
}),
|
|
661
|
+
] as const;
|
|
662
|
+
|
|
663
|
+
vi.mocked(global.fetch).mockImplementation(async (input) => {
|
|
664
|
+
assert(typeof input === "string");
|
|
665
|
+
const [, id] = String(input).match(/\/users\/([^/]+)/) ?? [];
|
|
666
|
+
|
|
667
|
+
return {
|
|
668
|
+
headers: new Headers(),
|
|
669
|
+
ok: true,
|
|
670
|
+
json: async () => ({ id: Number(id), name: "Updated Name" }),
|
|
671
|
+
} as Response;
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
const client = createClientBuilder(testFragmentDefinition, clientConfig, testRoutes);
|
|
675
|
+
const clientObj = {
|
|
676
|
+
updateUser: client.createMutator("PUT", "/users/:id"),
|
|
677
|
+
};
|
|
678
|
+
|
|
679
|
+
const { updateUser } = useFragno(clientObj);
|
|
680
|
+
const { mutate, data, error } = updateUser();
|
|
681
|
+
|
|
682
|
+
const userId = ref("42");
|
|
683
|
+
const result = await mutate({
|
|
684
|
+
body: { name: "Updated Name" },
|
|
685
|
+
path: { id: userId },
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
expect(result).toEqual({ id: 42, name: "Updated Name" });
|
|
689
|
+
expect(data.value).toEqual({ id: 42, name: "Updated Name" });
|
|
690
|
+
expect(error.value).toBeUndefined();
|
|
691
|
+
|
|
692
|
+
// Update the ref and call again
|
|
693
|
+
userId.value = "100";
|
|
694
|
+
const result2 = await mutate({
|
|
695
|
+
body: { name: "Another Name" },
|
|
696
|
+
path: { id: userId },
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
expect(result2).toEqual({ id: 100, name: "Updated Name" });
|
|
700
|
+
});
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
describe("useFragno", () => {
|
|
704
|
+
const clientConfig: FragnoPublicClientConfig = {
|
|
705
|
+
baseUrl: "http://localhost:3000",
|
|
706
|
+
};
|
|
707
|
+
|
|
708
|
+
const testFragmentDefinition = defineFragment("test-fragment");
|
|
709
|
+
const testRoutes = [
|
|
710
|
+
defineRoute({
|
|
711
|
+
method: "GET",
|
|
712
|
+
path: "/data",
|
|
713
|
+
outputSchema: z.string(),
|
|
714
|
+
handler: async (_ctx, { json }) => json("test data"),
|
|
715
|
+
}),
|
|
716
|
+
defineRoute({
|
|
717
|
+
method: "POST",
|
|
718
|
+
path: "/action",
|
|
719
|
+
inputSchema: z.object({ value: z.string() }),
|
|
720
|
+
outputSchema: z.object({ result: z.string() }),
|
|
721
|
+
handler: async (_ctx, { json }) => json({ result: "test value" }),
|
|
722
|
+
}),
|
|
723
|
+
] as const;
|
|
724
|
+
|
|
725
|
+
test("should pass through non-hook values unchanged", () => {
|
|
726
|
+
const client = createClientBuilder(testFragmentDefinition, clientConfig, testRoutes);
|
|
727
|
+
const clientObj = {
|
|
728
|
+
useData: client.createHook("/data"),
|
|
729
|
+
usePostAction: client.createMutator("POST", "/action"),
|
|
730
|
+
someString: "hello world",
|
|
731
|
+
someNumber: 42,
|
|
732
|
+
someObject: { foo: "bar", nested: { value: true } },
|
|
733
|
+
someArray: [1, 2, 3],
|
|
734
|
+
someFunction: () => "test",
|
|
735
|
+
someNull: null,
|
|
736
|
+
someUndefined: undefined,
|
|
737
|
+
};
|
|
738
|
+
|
|
739
|
+
const result = useFragno(clientObj);
|
|
740
|
+
|
|
741
|
+
// Check that non-hook values are passed through unchanged
|
|
742
|
+
expect(result.someString).toBe("hello world");
|
|
743
|
+
expect(result.someNumber).toBe(42);
|
|
744
|
+
expect(result.someObject).toEqual({ foo: "bar", nested: { value: true } });
|
|
745
|
+
expect(result.someArray).toEqual([1, 2, 3]);
|
|
746
|
+
expect(result.someFunction()).toBe("test");
|
|
747
|
+
expect(result.someNull).toBeNull();
|
|
748
|
+
expect(result.someUndefined).toBeUndefined();
|
|
749
|
+
|
|
750
|
+
// Verify that hooks are still transformed
|
|
751
|
+
expect(typeof result.useData).toBe("function");
|
|
752
|
+
expect(typeof result.usePostAction).toBe("function");
|
|
753
|
+
});
|
|
754
|
+
});
|