@fragno-dev/core 0.1.8 → 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +131 -56
- package/CHANGELOG.md +13 -0
- package/dist/api/api.d.ts +38 -2
- package/dist/api/api.d.ts.map +1 -0
- package/dist/api/api.js +9 -3
- package/dist/api/api.js.map +1 -0
- package/dist/api/bind-services.d.ts +6 -0
- package/dist/api/bind-services.d.ts.map +1 -0
- package/dist/api/bind-services.js +20 -0
- package/dist/api/bind-services.js.map +1 -0
- package/dist/api/error.d.ts +26 -0
- package/dist/api/error.d.ts.map +1 -0
- package/dist/api/error.js +48 -0
- package/dist/api/error.js.map +1 -0
- package/dist/api/fragment-definition-builder.d.ts +313 -0
- package/dist/api/fragment-definition-builder.d.ts.map +1 -0
- package/dist/api/fragment-definition-builder.js +326 -0
- package/dist/api/fragment-definition-builder.js.map +1 -0
- package/dist/api/fragment-instantiator.d.ts +216 -0
- package/dist/api/fragment-instantiator.d.ts.map +1 -0
- package/dist/api/fragment-instantiator.js +487 -0
- package/dist/api/fragment-instantiator.js.map +1 -0
- package/dist/api/fragno-response.d.ts +30 -0
- package/dist/api/fragno-response.d.ts.map +1 -0
- package/dist/api/fragno-response.js +73 -0
- package/dist/api/fragno-response.js.map +1 -0
- package/dist/api/internal/path.d.ts +50 -0
- package/dist/api/internal/path.d.ts.map +1 -0
- package/dist/api/internal/path.js +76 -0
- package/dist/api/internal/path.js.map +1 -0
- package/dist/api/internal/response-stream.d.ts +43 -0
- package/dist/api/internal/response-stream.d.ts.map +1 -0
- package/dist/api/internal/response-stream.js +81 -0
- package/dist/api/internal/response-stream.js.map +1 -0
- package/dist/api/internal/route.js +10 -0
- package/dist/api/internal/route.js.map +1 -0
- package/dist/api/mutable-request-state.d.ts +82 -0
- package/dist/api/mutable-request-state.d.ts.map +1 -0
- package/dist/api/mutable-request-state.js +97 -0
- package/dist/api/mutable-request-state.js.map +1 -0
- package/dist/api/request-context-storage.d.ts +42 -0
- package/dist/api/request-context-storage.d.ts.map +1 -0
- package/dist/api/request-context-storage.js +43 -0
- package/dist/api/request-context-storage.js.map +1 -0
- package/dist/api/request-input-context.d.ts +89 -0
- package/dist/api/request-input-context.d.ts.map +1 -0
- package/dist/api/request-input-context.js +118 -0
- package/dist/api/request-input-context.js.map +1 -0
- package/dist/api/request-middleware.d.ts +50 -0
- package/dist/api/request-middleware.d.ts.map +1 -0
- package/dist/api/request-middleware.js +83 -0
- package/dist/api/request-middleware.js.map +1 -0
- package/dist/api/request-output-context.d.ts +41 -0
- package/dist/api/request-output-context.d.ts.map +1 -0
- package/dist/api/request-output-context.js +119 -0
- package/dist/api/request-output-context.js.map +1 -0
- package/dist/api/route-handler-input-options.d.ts +21 -0
- package/dist/api/route-handler-input-options.d.ts.map +1 -0
- package/dist/api/route.d.ts +54 -2
- package/dist/api/route.d.ts.map +1 -0
- package/dist/api/route.js +29 -2
- package/dist/api/route.js.map +1 -0
- package/dist/api/shared-types.d.ts +47 -0
- package/dist/api/shared-types.d.ts.map +1 -0
- package/dist/api/shared-types.js +1 -0
- package/dist/client/client-error.d.ts +60 -0
- package/dist/client/client-error.d.ts.map +1 -0
- package/dist/client/client-error.js +92 -0
- package/dist/client/client-error.js.map +1 -0
- package/dist/client/client.d.ts +210 -2
- package/dist/client/client.d.ts.map +1 -0
- package/dist/client/client.js +397 -5
- package/dist/client/client.js.map +1 -0
- package/dist/client/client.svelte.d.ts +5 -2
- package/dist/client/client.svelte.d.ts.map +1 -1
- package/dist/client/client.svelte.js +1 -4
- package/dist/client/client.svelte.js.map +1 -1
- package/dist/client/internal/fetcher-merge.js +36 -0
- package/dist/client/internal/fetcher-merge.js.map +1 -0
- package/dist/client/internal/ndjson-streaming.js +139 -0
- package/dist/client/internal/ndjson-streaming.js.map +1 -0
- package/dist/client/react.d.ts +5 -2
- package/dist/client/react.d.ts.map +1 -1
- package/dist/client/react.js +3 -4
- package/dist/client/react.js.map +1 -1
- package/dist/client/solid.d.ts +5 -2
- package/dist/client/solid.d.ts.map +1 -1
- package/dist/client/solid.js +2 -4
- package/dist/client/solid.js.map +1 -1
- package/dist/client/vanilla.d.ts +5 -2
- package/dist/client/vanilla.d.ts.map +1 -1
- package/dist/client/vanilla.js +2 -42
- package/dist/client/vanilla.js.map +1 -1
- package/dist/client/vue.d.ts +5 -2
- package/dist/client/vue.d.ts.map +1 -1
- package/dist/client/vue.js +1 -4
- package/dist/client/vue.js.map +1 -1
- package/dist/http/http-status.d.ts +26 -0
- package/dist/http/http-status.d.ts.map +1 -0
- package/dist/integrations/react-ssr.js +1 -1
- package/dist/internal/symbols.d.ts +9 -0
- package/dist/internal/symbols.d.ts.map +1 -0
- package/dist/internal/symbols.js +10 -0
- package/dist/internal/symbols.js.map +1 -0
- package/dist/mod-client.d.ts +36 -0
- package/dist/mod-client.d.ts.map +1 -0
- package/dist/mod-client.js +21 -0
- package/dist/mod-client.js.map +1 -0
- package/dist/mod.d.ts +7 -2
- package/dist/mod.js +4 -4
- package/dist/request/request.d.ts +4 -0
- package/dist/request/request.js +5 -0
- package/dist/test/test.d.ts +62 -34
- package/dist/test/test.d.ts.map +1 -1
- package/dist/test/test.js +75 -42
- package/dist/test/test.js.map +1 -1
- package/dist/util/async.js +40 -0
- package/dist/util/async.js.map +1 -0
- package/dist/util/content-type.js +49 -0
- package/dist/util/content-type.js.map +1 -0
- package/dist/util/nanostores.js +31 -0
- package/dist/util/nanostores.js.map +1 -0
- package/dist/{ssr-kyKI7pqH.js → util/ssr.js} +2 -2
- package/dist/util/ssr.js.map +1 -0
- package/dist/util/types-util.d.ts +8 -0
- package/dist/util/types-util.d.ts.map +1 -0
- package/package.json +19 -12
- package/src/api/api.ts +1 -5
- package/src/api/bind-services.ts +42 -0
- package/src/api/fragment-definition-builder.extend.test.ts +810 -0
- package/src/api/fragment-definition-builder.test.ts +499 -0
- package/src/api/fragment-definition-builder.ts +1088 -0
- package/src/api/fragment-instantiator.test.ts +1488 -0
- package/src/api/fragment-instantiator.ts +1053 -0
- package/src/api/fragment-services.test.ts +454 -189
- package/src/api/request-context-storage.ts +64 -0
- package/src/api/request-middleware.test.ts +301 -228
- package/src/api/route.test.ts +12 -36
- package/src/api/route.ts +167 -155
- package/src/api/shared-types.ts +43 -0
- package/src/client/client-builder.test.ts +23 -23
- package/src/client/client.ssr.test.ts +3 -3
- package/src/client/client.svelte.test.ts +15 -15
- package/src/client/client.test.ts +22 -22
- package/src/client/client.ts +72 -12
- package/src/client/internal/fetcher-merge.ts +1 -1
- package/src/client/react.test.ts +2 -2
- package/src/client/solid.test.ts +2 -2
- package/src/client/vanilla.test.ts +2 -2
- package/src/client/vue.test.ts +2 -2
- package/src/internal/symbols.ts +5 -0
- package/src/mod-client.ts +59 -0
- package/src/mod.ts +22 -15
- package/src/request/request.ts +8 -0
- package/src/test/test.test.ts +189 -375
- package/src/test/test.ts +186 -152
- package/tsdown.config.ts +8 -5
- package/dist/api/fragment-builder.d.ts +0 -2
- package/dist/api/fragment-builder.js +0 -3
- package/dist/api/fragment-instantiation.d.ts +0 -2
- package/dist/api/fragment-instantiation.js +0 -4
- package/dist/api-BFrUCIsF.d.ts +0 -963
- package/dist/api-BFrUCIsF.d.ts.map +0 -1
- package/dist/client-DAFHcKqA.js +0 -782
- package/dist/client-DAFHcKqA.js.map +0 -1
- package/dist/fragment-builder-Boh2vNHq.js +0 -108
- package/dist/fragment-builder-Boh2vNHq.js.map +0 -1
- package/dist/fragment-instantiation-DUT-HLl1.js +0 -898
- package/dist/fragment-instantiation-DUT-HLl1.js.map +0 -1
- package/dist/route-C4CyNHkC.js +0 -26
- package/dist/route-C4CyNHkC.js.map +0 -1
- package/dist/ssr-kyKI7pqH.js.map +0 -1
- package/src/api/fragment-builder.ts +0 -518
- package/src/api/fragment-instantiation.test.ts +0 -702
- package/src/api/fragment-instantiation.ts +0 -766
- package/src/api/fragment.test.ts +0 -585
|
@@ -1,702 +0,0 @@
|
|
|
1
|
-
import { test, expect, describe } from "vitest";
|
|
2
|
-
import { defineFragment } from "./fragment-builder";
|
|
3
|
-
import { createFragment, instantiateFragment } from "./fragment-instantiation";
|
|
4
|
-
import { defineRoute, defineRoutes } from "./route";
|
|
5
|
-
import { z } from "zod";
|
|
6
|
-
|
|
7
|
-
describe("callRoute", () => {
|
|
8
|
-
test("calls route handler with body", async () => {
|
|
9
|
-
const config = { greeting: "Hello" };
|
|
10
|
-
|
|
11
|
-
const fragment = defineFragment<typeof config>("test-fragment");
|
|
12
|
-
|
|
13
|
-
const routesFactory = defineRoutes<typeof config>().create(() => {
|
|
14
|
-
return [
|
|
15
|
-
defineRoute({
|
|
16
|
-
method: "POST",
|
|
17
|
-
path: "/greet",
|
|
18
|
-
inputSchema: z.object({ name: z.string() }),
|
|
19
|
-
outputSchema: z.object({ message: z.string() }),
|
|
20
|
-
handler: async ({ input }, { json }) => {
|
|
21
|
-
const { name } = await input.valid();
|
|
22
|
-
return json({ message: `Hello, ${name}!` });
|
|
23
|
-
},
|
|
24
|
-
}),
|
|
25
|
-
];
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
const instance = createFragment(fragment, config, [routesFactory], {});
|
|
29
|
-
|
|
30
|
-
const response = await instance.callRoute("POST", "/greet", {
|
|
31
|
-
body: { name: "World" },
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
// expect(response.type).toBe("json");
|
|
35
|
-
if (response.type === "json") {
|
|
36
|
-
expect(response.status).toBe(200);
|
|
37
|
-
expect(response.data).toEqual({ message: "Hello, World!" });
|
|
38
|
-
}
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
test("calls route handler with path params", async () => {
|
|
42
|
-
const config = {};
|
|
43
|
-
|
|
44
|
-
const fragment = defineFragment<typeof config>("test-fragment");
|
|
45
|
-
|
|
46
|
-
const routesFactory = defineRoutes<typeof config>().create(() => {
|
|
47
|
-
return [
|
|
48
|
-
defineRoute({
|
|
49
|
-
method: "GET",
|
|
50
|
-
path: "/users/:id",
|
|
51
|
-
outputSchema: z.object({ userId: z.string() }),
|
|
52
|
-
handler: async ({ pathParams }, { json }) => {
|
|
53
|
-
return json({ userId: pathParams.id });
|
|
54
|
-
},
|
|
55
|
-
}),
|
|
56
|
-
];
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
const instance = createFragment(fragment, config, [routesFactory], {});
|
|
60
|
-
|
|
61
|
-
const response = await instance.callRoute("GET", "/users/:id", {
|
|
62
|
-
pathParams: { id: "123" },
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
expect(response.type).toBe("json");
|
|
66
|
-
if (response.type === "json") {
|
|
67
|
-
expect(response.status).toBe(200);
|
|
68
|
-
expect(response.data).toEqual({ userId: "123" });
|
|
69
|
-
}
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
test("calls route handler with query parameters", async () => {
|
|
73
|
-
const config = {};
|
|
74
|
-
|
|
75
|
-
const fragment = defineFragment<typeof config>("test-fragment");
|
|
76
|
-
|
|
77
|
-
const routesFactory = defineRoutes<typeof config>().create(() => {
|
|
78
|
-
return [
|
|
79
|
-
defineRoute({
|
|
80
|
-
method: "GET",
|
|
81
|
-
path: "/search",
|
|
82
|
-
queryParameters: ["q", "limit"],
|
|
83
|
-
outputSchema: z.object({ query: z.string(), limit: z.string().nullable() }),
|
|
84
|
-
handler: async ({ query }, { json }) => {
|
|
85
|
-
return json({
|
|
86
|
-
query: query.get("q") || "",
|
|
87
|
-
limit: query.get("limit"),
|
|
88
|
-
});
|
|
89
|
-
},
|
|
90
|
-
}),
|
|
91
|
-
];
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
const instance = createFragment(fragment, config, [routesFactory], {});
|
|
95
|
-
|
|
96
|
-
const response = await instance.callRoute("GET", "/search", {
|
|
97
|
-
query: { q: "test", limit: "10" },
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
expect(response.type).toBe("json");
|
|
101
|
-
if (response.type === "json") {
|
|
102
|
-
expect(response.status).toBe(200);
|
|
103
|
-
expect(response.data).toEqual({ query: "test", limit: "10" });
|
|
104
|
-
}
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
test("calls route handler with URLSearchParams query", async () => {
|
|
108
|
-
const config = {};
|
|
109
|
-
|
|
110
|
-
const fragment = defineFragment<typeof config>("test-fragment");
|
|
111
|
-
|
|
112
|
-
const routesFactory = defineRoutes<typeof config>().create(() => {
|
|
113
|
-
return [
|
|
114
|
-
defineRoute({
|
|
115
|
-
method: "GET",
|
|
116
|
-
path: "/search",
|
|
117
|
-
queryParameters: ["q"],
|
|
118
|
-
outputSchema: z.object({ query: z.string() }),
|
|
119
|
-
handler: async ({ query }, { json }) => {
|
|
120
|
-
return json({ query: query.get("q") || "" });
|
|
121
|
-
},
|
|
122
|
-
}),
|
|
123
|
-
];
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
const instance = createFragment(fragment, config, [routesFactory], {});
|
|
127
|
-
|
|
128
|
-
const searchParams = new URLSearchParams({ q: "test-query" });
|
|
129
|
-
const response = await instance.callRoute("GET", "/search", {
|
|
130
|
-
query: searchParams,
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
expect(response.type).toBe("json");
|
|
134
|
-
if (response.type === "json") {
|
|
135
|
-
expect(response.status).toBe(200);
|
|
136
|
-
expect(response.data).toEqual({ query: "test-query" });
|
|
137
|
-
}
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
test("calls route handler with headers", async () => {
|
|
141
|
-
const config = {};
|
|
142
|
-
|
|
143
|
-
const fragment = defineFragment<typeof config>("test-fragment");
|
|
144
|
-
|
|
145
|
-
const routesFactory = defineRoutes<typeof config>().create(() => {
|
|
146
|
-
return [
|
|
147
|
-
defineRoute({
|
|
148
|
-
method: "GET",
|
|
149
|
-
path: "/headers",
|
|
150
|
-
outputSchema: z.object({ auth: z.string().nullable() }),
|
|
151
|
-
handler: async ({ headers }, { json }) => {
|
|
152
|
-
return json({ auth: headers.get("authorization") });
|
|
153
|
-
},
|
|
154
|
-
}),
|
|
155
|
-
];
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
const instance = createFragment(fragment, config, [routesFactory], {});
|
|
159
|
-
|
|
160
|
-
const response = await instance.callRoute("GET", "/headers", {
|
|
161
|
-
headers: { authorization: "Bearer token123" },
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
expect(response.type).toBe("json");
|
|
165
|
-
if (response.type === "json") {
|
|
166
|
-
expect(response.status).toBe(200);
|
|
167
|
-
expect(response.data).toEqual({ auth: "Bearer token123" });
|
|
168
|
-
}
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
test("calls route handler with Headers object", async () => {
|
|
172
|
-
const config = {};
|
|
173
|
-
|
|
174
|
-
const fragment = defineFragment<typeof config>("test-fragment");
|
|
175
|
-
|
|
176
|
-
const routesFactory = defineRoutes<typeof config>().create(() => {
|
|
177
|
-
return [
|
|
178
|
-
defineRoute({
|
|
179
|
-
method: "GET",
|
|
180
|
-
path: "/headers",
|
|
181
|
-
outputSchema: z.object({ auth: z.string().nullable() }),
|
|
182
|
-
handler: async ({ headers }, { json }) => {
|
|
183
|
-
return json({ auth: headers.get("authorization") });
|
|
184
|
-
},
|
|
185
|
-
}),
|
|
186
|
-
];
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
const instance = createFragment(fragment, config, [routesFactory], {});
|
|
190
|
-
|
|
191
|
-
const requestHeaders = new Headers({ authorization: "Bearer token456" });
|
|
192
|
-
const response = await instance.callRoute("GET", "/headers", {
|
|
193
|
-
headers: requestHeaders,
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
expect(response.type).toBe("json");
|
|
197
|
-
if (response.type === "json") {
|
|
198
|
-
expect(response.status).toBe(200);
|
|
199
|
-
expect(response.data).toEqual({ auth: "Bearer token456" });
|
|
200
|
-
}
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
test("preserves response headers including Set-Cookie", async () => {
|
|
204
|
-
const config = {};
|
|
205
|
-
|
|
206
|
-
const fragment = defineFragment<typeof config>("test-fragment");
|
|
207
|
-
|
|
208
|
-
const routesFactory = defineRoutes<typeof config>().create(() => {
|
|
209
|
-
return [
|
|
210
|
-
defineRoute({
|
|
211
|
-
method: "POST",
|
|
212
|
-
path: "/login",
|
|
213
|
-
inputSchema: z.object({ username: z.string() }),
|
|
214
|
-
outputSchema: z.object({ success: z.boolean() }),
|
|
215
|
-
handler: async ({ input }, { json }) => {
|
|
216
|
-
const { username } = await input.valid();
|
|
217
|
-
const response = json({ success: true });
|
|
218
|
-
response.headers.set("Set-Cookie", `session=${username}; HttpOnly; Path=/`);
|
|
219
|
-
response.headers.set("X-Custom-Header", "custom-value");
|
|
220
|
-
return response;
|
|
221
|
-
},
|
|
222
|
-
}),
|
|
223
|
-
];
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
const instance = createFragment(fragment, config, [routesFactory], {});
|
|
227
|
-
|
|
228
|
-
const response = await instance.callRoute("POST", "/login", {
|
|
229
|
-
body: { username: "testuser" },
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
expect(response.type).toBe("json");
|
|
233
|
-
if (response.type === "json") {
|
|
234
|
-
expect(response.status).toBe(200);
|
|
235
|
-
expect(response.headers.get("Set-Cookie")).toBe("session=testuser; HttpOnly; Path=/");
|
|
236
|
-
expect(response.headers.get("X-Custom-Header")).toBe("custom-value");
|
|
237
|
-
expect(response.data).toEqual({ success: true });
|
|
238
|
-
}
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
test("validates input and returns error for invalid data", async () => {
|
|
242
|
-
const config = {};
|
|
243
|
-
|
|
244
|
-
const fragment = defineFragment<typeof config>("test-fragment");
|
|
245
|
-
|
|
246
|
-
const routesFactory = defineRoutes<typeof config>().create(() => {
|
|
247
|
-
return [
|
|
248
|
-
defineRoute({
|
|
249
|
-
method: "POST",
|
|
250
|
-
path: "/validate",
|
|
251
|
-
inputSchema: z.object({ age: z.number().min(18) }),
|
|
252
|
-
outputSchema: z.object({ valid: z.boolean() }),
|
|
253
|
-
handler: async ({ input }, { json }) => {
|
|
254
|
-
const { age } = await input.valid();
|
|
255
|
-
return json({ valid: age >= 18 });
|
|
256
|
-
},
|
|
257
|
-
}),
|
|
258
|
-
];
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
const instance = createFragment(fragment, config, [routesFactory], {});
|
|
262
|
-
|
|
263
|
-
const response = await instance.callRoute("POST", "/validate", {
|
|
264
|
-
body: { age: 15 },
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
expect(response.type).toBe("error");
|
|
268
|
-
if (response.type === "error") {
|
|
269
|
-
expect(response.status).toBe(400);
|
|
270
|
-
expect(response.error.code).toBe("FRAGNO_VALIDATION_ERROR");
|
|
271
|
-
}
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
test("handles errors thrown in route handler", async () => {
|
|
275
|
-
const config = {};
|
|
276
|
-
|
|
277
|
-
const fragment = defineFragment<typeof config>("test-fragment");
|
|
278
|
-
|
|
279
|
-
const routesFactory = defineRoutes<typeof config>().create(() => {
|
|
280
|
-
return [
|
|
281
|
-
defineRoute({
|
|
282
|
-
method: "GET",
|
|
283
|
-
path: "/error",
|
|
284
|
-
outputSchema: z.object({ result: z.string() }),
|
|
285
|
-
handler: async () => {
|
|
286
|
-
throw new Error("Unexpected error");
|
|
287
|
-
},
|
|
288
|
-
}),
|
|
289
|
-
];
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
const instance = createFragment(fragment, config, [routesFactory], {});
|
|
293
|
-
|
|
294
|
-
const response = await instance.callRoute("GET", "/error");
|
|
295
|
-
|
|
296
|
-
expect(response.type).toBe("error");
|
|
297
|
-
if (response.type === "error") {
|
|
298
|
-
expect(response.status).toBe(500);
|
|
299
|
-
expect(response.error).toEqual({
|
|
300
|
-
message: "Internal server error",
|
|
301
|
-
code: "INTERNAL_SERVER_ERROR",
|
|
302
|
-
});
|
|
303
|
-
}
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
test("calls route handler with all parameters combined", async () => {
|
|
307
|
-
const config = {};
|
|
308
|
-
|
|
309
|
-
const fragment = defineFragment<typeof config>("test-fragment");
|
|
310
|
-
|
|
311
|
-
const routesFactory = defineRoutes<typeof config>().create(() => {
|
|
312
|
-
return [
|
|
313
|
-
defineRoute({
|
|
314
|
-
method: "POST",
|
|
315
|
-
path: "/users/:id/update",
|
|
316
|
-
inputSchema: z.object({ name: z.string() }),
|
|
317
|
-
queryParameters: ["reason"],
|
|
318
|
-
outputSchema: z.object({
|
|
319
|
-
id: z.string(),
|
|
320
|
-
name: z.string(),
|
|
321
|
-
reason: z.string().nullable(),
|
|
322
|
-
auth: z.string().nullable(),
|
|
323
|
-
}),
|
|
324
|
-
handler: async ({ pathParams, input, query, headers }, { json }) => {
|
|
325
|
-
const { name } = await input.valid();
|
|
326
|
-
return json({
|
|
327
|
-
id: pathParams.id,
|
|
328
|
-
name,
|
|
329
|
-
reason: query.get("reason"),
|
|
330
|
-
auth: headers.get("authorization"),
|
|
331
|
-
});
|
|
332
|
-
},
|
|
333
|
-
}),
|
|
334
|
-
];
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
const instance = createFragment(fragment, config, [routesFactory], {});
|
|
338
|
-
|
|
339
|
-
const response = await instance.callRoute("POST", "/users/:id/update", {
|
|
340
|
-
pathParams: { id: "user123" },
|
|
341
|
-
body: { name: "John Doe" },
|
|
342
|
-
query: { reason: "profile-update" },
|
|
343
|
-
headers: { authorization: "Bearer xyz" },
|
|
344
|
-
});
|
|
345
|
-
|
|
346
|
-
expect(response.type).toBe("json");
|
|
347
|
-
if (response.type === "json") {
|
|
348
|
-
expect(response.status).toBe(200);
|
|
349
|
-
expect(response.data).toEqual({
|
|
350
|
-
id: "user123",
|
|
351
|
-
name: "John Doe",
|
|
352
|
-
reason: "profile-update",
|
|
353
|
-
auth: "Bearer xyz",
|
|
354
|
-
});
|
|
355
|
-
}
|
|
356
|
-
});
|
|
357
|
-
|
|
358
|
-
test("calls route handler with no input options", async () => {
|
|
359
|
-
const config = {};
|
|
360
|
-
|
|
361
|
-
const fragment = defineFragment<typeof config>("test-fragment");
|
|
362
|
-
|
|
363
|
-
const routesFactory = defineRoutes<typeof config>().create(() => {
|
|
364
|
-
return [
|
|
365
|
-
defineRoute({
|
|
366
|
-
method: "GET",
|
|
367
|
-
path: "/ping",
|
|
368
|
-
outputSchema: z.object({ status: z.string() }),
|
|
369
|
-
handler: async (_, { json }) => {
|
|
370
|
-
return json({ status: "ok" });
|
|
371
|
-
},
|
|
372
|
-
}),
|
|
373
|
-
];
|
|
374
|
-
});
|
|
375
|
-
|
|
376
|
-
const instance = createFragment(fragment, config, [routesFactory], {});
|
|
377
|
-
|
|
378
|
-
const response = await instance.callRoute("GET", "/ping");
|
|
379
|
-
|
|
380
|
-
expect(response.type).toBe("json");
|
|
381
|
-
if (response.type === "json") {
|
|
382
|
-
expect(response.status).toBe(200);
|
|
383
|
-
expect(response.data).toEqual({ status: "ok" });
|
|
384
|
-
}
|
|
385
|
-
});
|
|
386
|
-
|
|
387
|
-
test("uses services in route handler called via callRoute", async () => {
|
|
388
|
-
const config = {};
|
|
389
|
-
|
|
390
|
-
type Services = {
|
|
391
|
-
getUserName: () => string;
|
|
392
|
-
};
|
|
393
|
-
|
|
394
|
-
const fragment = defineFragment<typeof config>("test-fragment").providesService(
|
|
395
|
-
({ defineService }) => {
|
|
396
|
-
return defineService({
|
|
397
|
-
getUserName: () => "Test User",
|
|
398
|
-
});
|
|
399
|
-
},
|
|
400
|
-
);
|
|
401
|
-
|
|
402
|
-
const routesFactory = defineRoutes<typeof config, {}, Services>().create(({ services }) => {
|
|
403
|
-
return [
|
|
404
|
-
defineRoute({
|
|
405
|
-
method: "GET",
|
|
406
|
-
path: "/me",
|
|
407
|
-
outputSchema: z.object({ name: z.string() }),
|
|
408
|
-
handler: async (_, { json }) => {
|
|
409
|
-
return json({ name: services.getUserName() });
|
|
410
|
-
},
|
|
411
|
-
}),
|
|
412
|
-
];
|
|
413
|
-
});
|
|
414
|
-
|
|
415
|
-
const instance = createFragment(fragment, config, [routesFactory], {});
|
|
416
|
-
|
|
417
|
-
const response = await instance.callRoute("GET", "/me");
|
|
418
|
-
|
|
419
|
-
expect(response.type).toBe("json");
|
|
420
|
-
if (response.type === "json") {
|
|
421
|
-
expect(response.status).toBe(200);
|
|
422
|
-
expect(response.data).toEqual({ name: "Test User" });
|
|
423
|
-
}
|
|
424
|
-
});
|
|
425
|
-
|
|
426
|
-
test("uses deps in route handler called via callRoute", async () => {
|
|
427
|
-
const config = {};
|
|
428
|
-
|
|
429
|
-
type Deps = {
|
|
430
|
-
database: { query: () => string };
|
|
431
|
-
};
|
|
432
|
-
|
|
433
|
-
const fragment = defineFragment<typeof config>("test-fragment").withDependencies(() => {
|
|
434
|
-
return {
|
|
435
|
-
database: { query: () => "database-result" },
|
|
436
|
-
};
|
|
437
|
-
});
|
|
438
|
-
|
|
439
|
-
const routesFactory = defineRoutes<typeof config, Deps>().create(({ deps }) => {
|
|
440
|
-
return [
|
|
441
|
-
defineRoute({
|
|
442
|
-
method: "GET",
|
|
443
|
-
path: "/data",
|
|
444
|
-
outputSchema: z.object({ result: z.string() }),
|
|
445
|
-
handler: async (_, { json }) => {
|
|
446
|
-
return json({ result: deps.database.query() });
|
|
447
|
-
},
|
|
448
|
-
}),
|
|
449
|
-
];
|
|
450
|
-
});
|
|
451
|
-
|
|
452
|
-
const instance = createFragment(fragment, config, [routesFactory], {});
|
|
453
|
-
|
|
454
|
-
const response = await instance.callRoute("GET", "/data");
|
|
455
|
-
|
|
456
|
-
expect(response.type).toBe("json");
|
|
457
|
-
if (response.type === "json") {
|
|
458
|
-
expect(response.status).toBe(200);
|
|
459
|
-
expect(response.data).toEqual({ result: "database-result" });
|
|
460
|
-
}
|
|
461
|
-
});
|
|
462
|
-
|
|
463
|
-
test("this context is RequestThisContext for standard fragments", async () => {
|
|
464
|
-
const config = {};
|
|
465
|
-
|
|
466
|
-
const fragment = defineFragment<typeof config>("test-fragment");
|
|
467
|
-
|
|
468
|
-
const routesFactory = defineRoutes<typeof config>().create(() => {
|
|
469
|
-
return [
|
|
470
|
-
defineRoute({
|
|
471
|
-
method: "GET",
|
|
472
|
-
path: "/context-test",
|
|
473
|
-
outputSchema: z.object({ contextType: z.string() }),
|
|
474
|
-
handler: async function (_, { json }) {
|
|
475
|
-
// this should be RequestThisContext (empty object by default)
|
|
476
|
-
expect(this).toBeDefined();
|
|
477
|
-
expect(typeof this).toBe("object");
|
|
478
|
-
return json({ contextType: "standard" });
|
|
479
|
-
},
|
|
480
|
-
}),
|
|
481
|
-
];
|
|
482
|
-
});
|
|
483
|
-
|
|
484
|
-
const instance = createFragment(fragment, config, [routesFactory], {});
|
|
485
|
-
|
|
486
|
-
const response = await instance.callRoute("GET", "/context-test");
|
|
487
|
-
|
|
488
|
-
expect(response.type).toBe("json");
|
|
489
|
-
if (response.type === "json") {
|
|
490
|
-
expect(response.status).toBe(200);
|
|
491
|
-
expect(response.data).toEqual({ contextType: "standard" });
|
|
492
|
-
}
|
|
493
|
-
});
|
|
494
|
-
|
|
495
|
-
test("route handlers receive correct this context at runtime", async () => {
|
|
496
|
-
const fragment = defineFragment("this-test");
|
|
497
|
-
|
|
498
|
-
const routesFactory = defineRoutes(fragment).create(({ defineRoute }) => {
|
|
499
|
-
return [
|
|
500
|
-
defineRoute({
|
|
501
|
-
method: "GET",
|
|
502
|
-
path: "/this-test",
|
|
503
|
-
outputSchema: z.object({ hasThis: z.boolean() }),
|
|
504
|
-
handler: async function (_, { json }) {
|
|
505
|
-
// Verify that 'this' is defined and is an object
|
|
506
|
-
const hasThis = this !== undefined && typeof this === "object";
|
|
507
|
-
return json({ hasThis });
|
|
508
|
-
},
|
|
509
|
-
}),
|
|
510
|
-
];
|
|
511
|
-
});
|
|
512
|
-
|
|
513
|
-
const instance = createFragment(fragment, {}, [routesFactory], {});
|
|
514
|
-
|
|
515
|
-
const response = await instance.callRoute("GET", "/this-test");
|
|
516
|
-
|
|
517
|
-
expect(response.type).toBe("json");
|
|
518
|
-
if (response.type === "json") {
|
|
519
|
-
expect(response.status).toBe(200);
|
|
520
|
-
expect(response.data).toEqual({ hasThis: true });
|
|
521
|
-
}
|
|
522
|
-
});
|
|
523
|
-
});
|
|
524
|
-
|
|
525
|
-
describe("FragmentInstantiationBuilder", () => {
|
|
526
|
-
describe("instantiate", () => {
|
|
527
|
-
test("basic instantiation with defaults", () => {
|
|
528
|
-
const fragment = defineFragment<{ apiKey: string }>("test");
|
|
529
|
-
|
|
530
|
-
const instance = instantiateFragment(fragment).build();
|
|
531
|
-
|
|
532
|
-
expect(instance.config.name).toBe("test");
|
|
533
|
-
expect(instance.deps).toEqual({});
|
|
534
|
-
expect(instance.services).toEqual({});
|
|
535
|
-
});
|
|
536
|
-
|
|
537
|
-
test("with config", () => {
|
|
538
|
-
const fragment = defineFragment<{ apiKey: string }>("test");
|
|
539
|
-
|
|
540
|
-
const instance = instantiateFragment(fragment).withConfig({ apiKey: "test-key" }).build();
|
|
541
|
-
|
|
542
|
-
expect(instance.config.name).toBe("test");
|
|
543
|
-
});
|
|
544
|
-
|
|
545
|
-
test("with routes", async () => {
|
|
546
|
-
const fragment = defineFragment<{}>("test");
|
|
547
|
-
|
|
548
|
-
const route = defineRoute({
|
|
549
|
-
method: "GET",
|
|
550
|
-
path: "/hello",
|
|
551
|
-
outputSchema: z.object({ message: z.string() }),
|
|
552
|
-
handler: async (_ctx, { json }) => json({ message: "hello" }),
|
|
553
|
-
});
|
|
554
|
-
|
|
555
|
-
const instance = instantiateFragment(fragment).withConfig({}).withRoutes([route]).build();
|
|
556
|
-
|
|
557
|
-
const response = await instance.callRoute("GET", "/hello");
|
|
558
|
-
|
|
559
|
-
expect(response.type).toBe("json");
|
|
560
|
-
if (response.type === "json") {
|
|
561
|
-
expect(response.data).toEqual({ message: "hello" });
|
|
562
|
-
}
|
|
563
|
-
});
|
|
564
|
-
|
|
565
|
-
test("with options", () => {
|
|
566
|
-
const fragment = defineFragment<{}>("test");
|
|
567
|
-
|
|
568
|
-
const instance = instantiateFragment(fragment)
|
|
569
|
-
.withConfig({})
|
|
570
|
-
.withOptions({ mountRoute: "/custom" })
|
|
571
|
-
.build();
|
|
572
|
-
|
|
573
|
-
expect(instance.mountRoute).toBe("/custom");
|
|
574
|
-
});
|
|
575
|
-
|
|
576
|
-
test("with services (used services)", () => {
|
|
577
|
-
interface IEmailService {
|
|
578
|
-
sendEmail(to: string, subject: string): Promise<void>;
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
const emailImpl: IEmailService = {
|
|
582
|
-
sendEmail: async () => {},
|
|
583
|
-
};
|
|
584
|
-
|
|
585
|
-
const fragment = defineFragment<{}>("test").usesService<"email", IEmailService>("email");
|
|
586
|
-
|
|
587
|
-
const instance = instantiateFragment(fragment)
|
|
588
|
-
.withConfig({})
|
|
589
|
-
.withServices({ email: emailImpl })
|
|
590
|
-
.build();
|
|
591
|
-
|
|
592
|
-
expect(instance.services.email).toBeDefined();
|
|
593
|
-
expect(instance.services.email.sendEmail).toBeDefined();
|
|
594
|
-
});
|
|
595
|
-
|
|
596
|
-
test("all options together", async () => {
|
|
597
|
-
interface ILogger {
|
|
598
|
-
log(message: string): void;
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
const loggerImpl: ILogger = {
|
|
602
|
-
log: () => {},
|
|
603
|
-
};
|
|
604
|
-
|
|
605
|
-
const fragment = defineFragment<{ apiKey: string }>("test")
|
|
606
|
-
.withDependencies(({ config }) => ({
|
|
607
|
-
client: { key: config.apiKey },
|
|
608
|
-
}))
|
|
609
|
-
.usesService<"logger", ILogger>("logger");
|
|
610
|
-
|
|
611
|
-
const route = defineRoute({
|
|
612
|
-
method: "GET",
|
|
613
|
-
path: "/test",
|
|
614
|
-
outputSchema: z.object({ success: z.boolean() }),
|
|
615
|
-
handler: async (_ctx, { json }) => json({ success: true }),
|
|
616
|
-
});
|
|
617
|
-
|
|
618
|
-
const instance = instantiateFragment(fragment)
|
|
619
|
-
.withConfig({ apiKey: "my-key" })
|
|
620
|
-
.withRoutes([route])
|
|
621
|
-
.withOptions({ mountRoute: "/api" })
|
|
622
|
-
.withServices({ logger: loggerImpl })
|
|
623
|
-
.build();
|
|
624
|
-
|
|
625
|
-
expect(instance.config.name).toBe("test");
|
|
626
|
-
expect(instance.mountRoute).toBe("/api");
|
|
627
|
-
expect(instance.deps.client.key).toBe("my-key");
|
|
628
|
-
expect(instance.services.logger).toBeDefined();
|
|
629
|
-
|
|
630
|
-
const response = await instance.callRoute("GET", "/test");
|
|
631
|
-
expect(response.type).toBe("json");
|
|
632
|
-
if (response.type === "json") {
|
|
633
|
-
expect(response.data).toEqual({ success: true });
|
|
634
|
-
}
|
|
635
|
-
});
|
|
636
|
-
|
|
637
|
-
test("method chaining is fluent", () => {
|
|
638
|
-
const fragment = defineFragment<{ apiKey: string }>("test");
|
|
639
|
-
|
|
640
|
-
// Should be chainable
|
|
641
|
-
const builder = instantiateFragment(fragment)
|
|
642
|
-
.withConfig({ apiKey: "key" })
|
|
643
|
-
.withRoutes([])
|
|
644
|
-
.withOptions({});
|
|
645
|
-
|
|
646
|
-
expect(builder).toBeDefined();
|
|
647
|
-
expect(typeof builder.build).toBe("function");
|
|
648
|
-
});
|
|
649
|
-
|
|
650
|
-
test("config defaults to empty object", () => {
|
|
651
|
-
const fragment = defineFragment<{}>("test");
|
|
652
|
-
|
|
653
|
-
const instance = instantiateFragment(fragment).build();
|
|
654
|
-
|
|
655
|
-
expect(instance).toBeDefined();
|
|
656
|
-
});
|
|
657
|
-
|
|
658
|
-
test("routes defaults to empty array", () => {
|
|
659
|
-
const fragment = defineFragment<{}>("test");
|
|
660
|
-
|
|
661
|
-
const instance = instantiateFragment(fragment).withConfig({}).build();
|
|
662
|
-
|
|
663
|
-
expect(instance.config.routes).toEqual([]);
|
|
664
|
-
});
|
|
665
|
-
|
|
666
|
-
test("options defaults to empty object", () => {
|
|
667
|
-
const fragment = defineFragment<{}>("test");
|
|
668
|
-
|
|
669
|
-
const instance = instantiateFragment(fragment).withConfig({}).build();
|
|
670
|
-
|
|
671
|
-
// Default mountRoute is generated
|
|
672
|
-
expect(instance.mountRoute).toContain("/test");
|
|
673
|
-
});
|
|
674
|
-
|
|
675
|
-
test("services are optional if not required", () => {
|
|
676
|
-
interface ILogger {
|
|
677
|
-
log(message: string): void;
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
const fragment = defineFragment<{}>("test").usesService<"logger", ILogger>("logger", {
|
|
681
|
-
optional: true,
|
|
682
|
-
});
|
|
683
|
-
|
|
684
|
-
// Should not throw even without providing the service
|
|
685
|
-
const instance = instantiateFragment(fragment).withConfig({}).build();
|
|
686
|
-
|
|
687
|
-
expect(instance).toBeDefined();
|
|
688
|
-
});
|
|
689
|
-
|
|
690
|
-
test("throws when required service not provided", () => {
|
|
691
|
-
interface IEmailService {
|
|
692
|
-
sendEmail(to: string): Promise<void>;
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
const fragment = defineFragment<{}>("test").usesService<"email", IEmailService>("email");
|
|
696
|
-
|
|
697
|
-
expect(() => {
|
|
698
|
-
instantiateFragment(fragment).withConfig({}).build();
|
|
699
|
-
}).toThrow("Fragment 'test' requires service 'email' but it was not provided");
|
|
700
|
-
});
|
|
701
|
-
});
|
|
702
|
-
});
|