@fragno-dev/core 0.1.1 → 0.1.3
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 +41 -32
- package/CHANGELOG.md +15 -0
- package/LICENSE.md +16 -0
- package/dist/api/api.d.ts +1 -1
- package/dist/api/fragment-builder.d.ts +2 -2
- package/dist/api/fragment-instantiation.d.ts +2 -2
- package/dist/api/fragment-instantiation.js +3 -2
- package/dist/{api-Dcr4_-3g.d.ts → api-BX90b4-D.d.ts} +92 -6
- package/dist/api-BX90b4-D.d.ts.map +1 -0
- package/dist/client/client.d.ts +2 -2
- package/dist/client/client.js +4 -3
- package/dist/client/client.svelte.d.ts +2 -2
- package/dist/client/client.svelte.js +4 -3
- package/dist/client/client.svelte.js.map +1 -1
- package/dist/client/react.d.ts +2 -2
- package/dist/client/react.d.ts.map +1 -1
- package/dist/client/react.js +4 -3
- package/dist/client/react.js.map +1 -1
- package/dist/client/solid.d.ts +2 -2
- package/dist/client/solid.js +4 -3
- package/dist/client/solid.js.map +1 -1
- package/dist/client/vanilla.d.ts +2 -2
- package/dist/client/vanilla.js +4 -3
- package/dist/client/vanilla.js.map +1 -1
- package/dist/client/vue.d.ts +2 -2
- package/dist/client/vue.js +4 -3
- package/dist/client/vue.js.map +1 -1
- package/dist/{client-D5ORmjBP.js → client-C6LChM0Y.js} +4 -3
- package/dist/client-C6LChM0Y.js.map +1 -0
- package/dist/{fragment-builder-D6-oLYnH.d.ts → fragment-builder-BZr2JkuW.d.ts} +51 -38
- package/dist/fragment-builder-BZr2JkuW.d.ts.map +1 -0
- package/dist/fragment-builder-DOnCVBqc.js.map +1 -1
- package/dist/{fragment-instantiation-f4AhwQss.js → fragment-instantiation-DMw8OKMC.js} +137 -11
- package/dist/fragment-instantiation-DMw8OKMC.js.map +1 -0
- package/dist/integrations/react-ssr.js +1 -1
- package/dist/mod.d.ts +2 -2
- package/dist/mod.js +3 -2
- package/dist/route-CTxjMtGZ.js +10 -0
- package/dist/route-CTxjMtGZ.js.map +1 -0
- package/dist/{route-B4RbOWjd.js → route-D1MZR6JL.js} +22 -22
- package/dist/route-D1MZR6JL.js.map +1 -0
- package/dist/{ssr-CamRrMc0.js → ssr-BByDVfFD.js} +1 -1
- package/dist/{ssr-CamRrMc0.js.map → ssr-BByDVfFD.js.map} +1 -1
- package/dist/test/test.d.ts +112 -0
- package/dist/test/test.d.ts.map +1 -0
- package/dist/test/test.js +155 -0
- package/dist/test/test.js.map +1 -0
- package/package.json +18 -24
- package/src/api/fragment-builder.ts +0 -1
- package/src/api/fragment-instantiation.ts +16 -3
- package/src/api/mutable-request-state.ts +107 -0
- package/src/api/request-input-context.test.ts +51 -0
- package/src/api/request-input-context.ts +20 -13
- package/src/api/request-middleware.test.ts +88 -2
- package/src/api/request-middleware.ts +28 -6
- package/src/api/request-output-context.test.ts +6 -2
- package/src/api/request-output-context.ts +15 -9
- package/src/client/component.test.svelte +2 -0
- package/src/client/internal/ndjson-streaming.ts +6 -2
- package/src/client/react.ts +3 -1
- package/src/test/test.test.ts +449 -0
- package/src/test/test.ts +379 -0
- package/src/util/async.test.ts +6 -2
- package/tsdown.config.ts +1 -0
- package/.turbo/turbo-test.log +0 -297
- package/.turbo/turbo-types$colon$check.log +0 -1
- package/dist/api-Dcr4_-3g.d.ts.map +0 -1
- package/dist/client-D5ORmjBP.js.map +0 -1
- package/dist/fragment-builder-D6-oLYnH.d.ts.map +0 -1
- package/dist/fragment-instantiation-f4AhwQss.js.map +0 -1
- package/dist/route-B4RbOWjd.js.map +0 -1
|
@@ -19,7 +19,9 @@ function mergeHeaders(...headerSources: (HeadersInit | undefined)[]): HeadersIni
|
|
|
19
19
|
const mergedHeaders = new Headers();
|
|
20
20
|
|
|
21
21
|
for (const headerSource of headerSources) {
|
|
22
|
-
if (!headerSource)
|
|
22
|
+
if (!headerSource) {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
23
25
|
|
|
24
26
|
if (headerSource instanceof Headers) {
|
|
25
27
|
for (const [key, value] of headerSource.entries()) {
|
|
@@ -45,11 +47,11 @@ export abstract class OutputContext<const TOutput, const TErrorCode extends stri
|
|
|
45
47
|
*
|
|
46
48
|
* Shortcut for `throw new FragnoApiError(...)`
|
|
47
49
|
*/
|
|
48
|
-
error(
|
|
50
|
+
error = (
|
|
49
51
|
{ message, code }: { message: string; code: TErrorCode },
|
|
50
52
|
initOrStatus?: ResponseInit | StatusCode,
|
|
51
53
|
headers?: HeadersInit,
|
|
52
|
-
): Response {
|
|
54
|
+
): Response => {
|
|
53
55
|
if (typeof initOrStatus === "undefined") {
|
|
54
56
|
return Response.json({ message: message, code }, { status: 500, headers });
|
|
55
57
|
}
|
|
@@ -63,12 +65,12 @@ export abstract class OutputContext<const TOutput, const TErrorCode extends stri
|
|
|
63
65
|
{ message: message, code },
|
|
64
66
|
{ status: initOrStatus.status, headers: mergedHeaders },
|
|
65
67
|
);
|
|
66
|
-
}
|
|
68
|
+
};
|
|
67
69
|
|
|
68
|
-
empty(
|
|
70
|
+
empty = (
|
|
69
71
|
initOrStatus?: ResponseInit<ContentlessStatusCode> | ContentlessStatusCode,
|
|
70
72
|
headers?: HeadersInit,
|
|
71
|
-
): Response {
|
|
73
|
+
): Response => {
|
|
72
74
|
const defaultHeaders = {};
|
|
73
75
|
|
|
74
76
|
if (typeof initOrStatus === "undefined") {
|
|
@@ -92,9 +94,13 @@ export abstract class OutputContext<const TOutput, const TErrorCode extends stri
|
|
|
92
94
|
status: initOrStatus.status,
|
|
93
95
|
headers: mergedHeaders,
|
|
94
96
|
});
|
|
95
|
-
}
|
|
97
|
+
};
|
|
96
98
|
|
|
97
|
-
json
|
|
99
|
+
json = (
|
|
100
|
+
object: TOutput,
|
|
101
|
+
initOrStatus?: ResponseInit | StatusCode,
|
|
102
|
+
headers?: HeadersInit,
|
|
103
|
+
): Response => {
|
|
98
104
|
if (typeof initOrStatus === "undefined") {
|
|
99
105
|
return Response.json(object, {
|
|
100
106
|
status: 200,
|
|
@@ -114,7 +120,7 @@ export abstract class OutputContext<const TOutput, const TErrorCode extends stri
|
|
|
114
120
|
status: initOrStatus.status,
|
|
115
121
|
headers: mergedHeaders,
|
|
116
122
|
});
|
|
117
|
-
}
|
|
123
|
+
};
|
|
118
124
|
|
|
119
125
|
jsonStream = (
|
|
120
126
|
cb: (stream: ResponseStream<TOutput>) => void | Promise<void>,
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
import type { Readable } from "svelte/store";
|
|
3
3
|
import { useFragno } from "./client.svelte";
|
|
4
4
|
|
|
5
|
+
// oxlint-disable-next-line no-unassigned-vars
|
|
5
6
|
export let clientObj: Record<string, unknown>;
|
|
7
|
+
// oxlint-disable-next-line no-unassigned-vars
|
|
6
8
|
export let hookName: string;
|
|
7
9
|
export let args: Record<string, unknown> = {};
|
|
8
10
|
|
|
@@ -191,7 +191,9 @@ async function continueStreaming<TOutputSchema extends StandardSchemaV1, TErrorC
|
|
|
191
191
|
if (buffer.trim()) {
|
|
192
192
|
const lines = buffer.split("\n");
|
|
193
193
|
for (const line of lines) {
|
|
194
|
-
if (!line.trim())
|
|
194
|
+
if (!line.trim()) {
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
195
197
|
|
|
196
198
|
try {
|
|
197
199
|
const jsonObject = JSON.parse(line) as StandardSchemaV1.InferOutput<TOutputSchema>;
|
|
@@ -215,7 +217,9 @@ async function continueStreaming<TOutputSchema extends StandardSchemaV1, TErrorC
|
|
|
215
217
|
buffer = lines.pop() || ""; // Keep incomplete line in buffer
|
|
216
218
|
|
|
217
219
|
for (const line of lines) {
|
|
218
|
-
if (!line.trim())
|
|
220
|
+
if (!line.trim()) {
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
219
223
|
|
|
220
224
|
try {
|
|
221
225
|
const jsonObject = JSON.parse(line) as StandardSchemaV1.InferOutput<TOutputSchema>;
|
package/src/client/react.ts
CHANGED
|
@@ -210,7 +210,9 @@ export function useStore<SomeStore extends Store>(
|
|
|
210
210
|
|
|
211
211
|
const subscribe = useCallback((onChange: () => void) => {
|
|
212
212
|
const emitChange = (value: StoreValue<SomeStore>) => {
|
|
213
|
-
if (snapshotRef.current === value)
|
|
213
|
+
if (snapshotRef.current === value) {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
214
216
|
snapshotRef.current = value;
|
|
215
217
|
onChange();
|
|
216
218
|
};
|
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { createFragmentForTest } from "./test";
|
|
3
|
+
import { defineFragment } from "../api/fragment-builder";
|
|
4
|
+
import { defineRoute } from "../api/route";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
|
|
7
|
+
describe("createFragmentForTest", () => {
|
|
8
|
+
it("should create a test fragment with config only", () => {
|
|
9
|
+
const fragment = defineFragment<{ apiKey: string }>("test");
|
|
10
|
+
const testFragment = createFragmentForTest(fragment, {
|
|
11
|
+
config: { apiKey: "test-key" },
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
expect(testFragment.config).toEqual({ apiKey: "test-key" });
|
|
15
|
+
expect(testFragment.deps).toEqual({});
|
|
16
|
+
expect(testFragment.services).toEqual({});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("should create deps from fragment definition", () => {
|
|
20
|
+
const fragment = defineFragment<{ apiKey: string }>("test").withDependencies(({ config }) => ({
|
|
21
|
+
client: { apiKey: config.apiKey },
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
const testFragment = createFragmentForTest(fragment, {
|
|
25
|
+
config: { apiKey: "test-key" },
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
expect(testFragment.deps).toEqual({ client: { apiKey: "test-key" } });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("should override deps when provided", () => {
|
|
32
|
+
const fragment = defineFragment<{ apiKey: string }>("test").withDependencies(({ config }) => ({
|
|
33
|
+
client: { apiKey: config.apiKey },
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
const testFragment = createFragmentForTest(fragment, {
|
|
37
|
+
config: { apiKey: "test-key" },
|
|
38
|
+
deps: { client: { apiKey: "override-key" } },
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
expect(testFragment.deps).toEqual({ client: { apiKey: "override-key" } });
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("should create services from fragment definition", () => {
|
|
45
|
+
const fragment = defineFragment<{ apiKey: string }>("test")
|
|
46
|
+
.withDependencies(({ config }) => ({
|
|
47
|
+
client: { apiKey: config.apiKey },
|
|
48
|
+
}))
|
|
49
|
+
.withServices(({ deps }) => ({
|
|
50
|
+
getApiKey: () => deps.client.apiKey,
|
|
51
|
+
}));
|
|
52
|
+
|
|
53
|
+
const testFragment = createFragmentForTest(fragment, {
|
|
54
|
+
config: { apiKey: "test-key" },
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
expect(testFragment.services.getApiKey()).toBe("test-key");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("should override services when provided", () => {
|
|
61
|
+
const fragment = defineFragment<{ apiKey: string }>("test")
|
|
62
|
+
.withDependencies(({ config }) => ({
|
|
63
|
+
client: { apiKey: config.apiKey },
|
|
64
|
+
}))
|
|
65
|
+
.withServices(({ deps }) => ({
|
|
66
|
+
getApiKey: () => deps.client.apiKey,
|
|
67
|
+
}));
|
|
68
|
+
|
|
69
|
+
const testFragment = createFragmentForTest(fragment, {
|
|
70
|
+
config: { apiKey: "test-key" },
|
|
71
|
+
services: { getApiKey: () => "override-key" },
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
expect(testFragment.services.getApiKey()).toBe("override-key");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("should initialize routes with fragment context", () => {
|
|
78
|
+
const fragment = defineFragment<{ multiplier: number }>("test")
|
|
79
|
+
.withDependencies(() => ({ dep: "value" }))
|
|
80
|
+
.withServices(({ config }) => ({
|
|
81
|
+
multiply: (x: number) => x * config.multiplier,
|
|
82
|
+
}));
|
|
83
|
+
|
|
84
|
+
const testFragment = createFragmentForTest(fragment, {
|
|
85
|
+
config: { multiplier: 2 },
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const route = defineRoute({
|
|
89
|
+
method: "GET",
|
|
90
|
+
path: "/test",
|
|
91
|
+
outputSchema: z.object({ result: z.number() }),
|
|
92
|
+
handler: async (_ctx, { json }) => {
|
|
93
|
+
return json({ result: 42 });
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const routes = [route] as const;
|
|
98
|
+
const [initializedRoute] = testFragment.initRoutes(routes);
|
|
99
|
+
|
|
100
|
+
expect(initializedRoute).toBe(route);
|
|
101
|
+
expect(initializedRoute.method).toBe("GET");
|
|
102
|
+
expect(initializedRoute.path).toBe("/test");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("should initialize route factories with fragment context", async () => {
|
|
106
|
+
const fragment = defineFragment<{ multiplier: number }>("test")
|
|
107
|
+
.withDependencies(() => ({ dep: "value" }))
|
|
108
|
+
.withServices(({ config }) => ({
|
|
109
|
+
multiply: (x: number) => x * config.multiplier,
|
|
110
|
+
}));
|
|
111
|
+
|
|
112
|
+
const testFragment = createFragmentForTest(fragment, {
|
|
113
|
+
config: { multiplier: 3 },
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const routeFactory = ({ services }: { services: { multiply: (x: number) => number } }) => {
|
|
117
|
+
return [
|
|
118
|
+
defineRoute({
|
|
119
|
+
method: "GET",
|
|
120
|
+
path: "/multiply",
|
|
121
|
+
outputSchema: z.object({ result: z.number() }),
|
|
122
|
+
handler: async (_ctx, { json }) => {
|
|
123
|
+
return json({ result: services.multiply(5) });
|
|
124
|
+
},
|
|
125
|
+
}),
|
|
126
|
+
];
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const routes = [routeFactory] as const;
|
|
130
|
+
const [multiplyRoute] = testFragment.initRoutes(routes);
|
|
131
|
+
|
|
132
|
+
expect(multiplyRoute.method).toBe("GET");
|
|
133
|
+
expect(multiplyRoute.path).toBe("/multiply");
|
|
134
|
+
|
|
135
|
+
// Test that the route was initialized with the correct services
|
|
136
|
+
const response = await testFragment.handler(multiplyRoute);
|
|
137
|
+
expect(response.type).toBe("json");
|
|
138
|
+
if (response.type === "json") {
|
|
139
|
+
expect(response.data).toEqual({ result: 15 }); // 5 * 3
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("should allow overriding config/deps/services for specific route initialization", async () => {
|
|
144
|
+
const fragment = defineFragment<{ multiplier: number }>("test")
|
|
145
|
+
.withDependencies(() => ({ baseUrl: "https://api.example.com" }))
|
|
146
|
+
.withServices(({ config }) => ({
|
|
147
|
+
multiply: (x: number) => x * config.multiplier,
|
|
148
|
+
getMessage: (): string => "original message",
|
|
149
|
+
}));
|
|
150
|
+
|
|
151
|
+
const testFragment = createFragmentForTest(fragment, {
|
|
152
|
+
config: { multiplier: 2 },
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const routeFactory = ({
|
|
156
|
+
config,
|
|
157
|
+
services,
|
|
158
|
+
}: {
|
|
159
|
+
config: { multiplier: number };
|
|
160
|
+
services: { multiply: (x: number) => number; getMessage: () => string };
|
|
161
|
+
}) => {
|
|
162
|
+
return [
|
|
163
|
+
defineRoute({
|
|
164
|
+
method: "GET",
|
|
165
|
+
path: "/test",
|
|
166
|
+
outputSchema: z.object({
|
|
167
|
+
result: z.number(),
|
|
168
|
+
message: z.string(),
|
|
169
|
+
multiplier: z.number(),
|
|
170
|
+
}),
|
|
171
|
+
handler: async (_ctx, { json }) => {
|
|
172
|
+
return json({
|
|
173
|
+
result: services.multiply(10),
|
|
174
|
+
message: services.getMessage(),
|
|
175
|
+
multiplier: config.multiplier,
|
|
176
|
+
});
|
|
177
|
+
},
|
|
178
|
+
}),
|
|
179
|
+
];
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const routes = [routeFactory] as const;
|
|
183
|
+
|
|
184
|
+
// Initialize with overrides - completely replace the multiply service
|
|
185
|
+
const [overriddenRoute] = testFragment.initRoutes(routes, {
|
|
186
|
+
config: { multiplier: 5 },
|
|
187
|
+
services: {
|
|
188
|
+
multiply: (x: number) => x * 5, // Mock implementation uses hardcoded multiplier
|
|
189
|
+
getMessage: (): string => "mocked message",
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
const response = await testFragment.handler(overriddenRoute);
|
|
194
|
+
expect(response.type).toBe("json");
|
|
195
|
+
if (response.type === "json") {
|
|
196
|
+
expect(response.data).toEqual({
|
|
197
|
+
result: 50, // 10 * 5 (mocked multiply service)
|
|
198
|
+
message: "mocked message", // overridden service
|
|
199
|
+
multiplier: 5, // overridden config
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Verify original fragment config/services are unchanged
|
|
204
|
+
expect(testFragment.config.multiplier).toBe(2);
|
|
205
|
+
expect(testFragment.services.multiply(10)).toBe(20); // Original multiplier is 2
|
|
206
|
+
expect(testFragment.services.getMessage()).toBe("original message");
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
describe("fragment.handler", () => {
|
|
211
|
+
it("should handle JSON response", async () => {
|
|
212
|
+
const fragment = defineFragment<{ apiKey: string }>("test");
|
|
213
|
+
const testFragment = createFragmentForTest(fragment, {
|
|
214
|
+
config: { apiKey: "test-key" },
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
const route = defineRoute({
|
|
218
|
+
method: "GET",
|
|
219
|
+
path: "/test",
|
|
220
|
+
outputSchema: z.object({ message: z.string() }),
|
|
221
|
+
handler: async (_ctx, { json }) => {
|
|
222
|
+
return json({ message: "hello" });
|
|
223
|
+
},
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
const response = await testFragment.handler(route);
|
|
227
|
+
|
|
228
|
+
expect(response.type).toBe("json");
|
|
229
|
+
if (response.type === "json") {
|
|
230
|
+
expect(response.status).toBe(200);
|
|
231
|
+
expect(response.data).toEqual({ message: "hello" });
|
|
232
|
+
expect(response.headers).toBeInstanceOf(Headers);
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it("should handle empty response", async () => {
|
|
237
|
+
const fragment = defineFragment<{ apiKey: string }>("test");
|
|
238
|
+
const testFragment = createFragmentForTest(fragment, {
|
|
239
|
+
config: { apiKey: "test-key" },
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
const route = defineRoute({
|
|
243
|
+
method: "DELETE",
|
|
244
|
+
path: "/test",
|
|
245
|
+
handler: async (_ctx, { empty }) => {
|
|
246
|
+
return empty(204);
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
const response = await testFragment.handler(route);
|
|
251
|
+
|
|
252
|
+
expect(response.type).toBe("empty");
|
|
253
|
+
if (response.type === "empty") {
|
|
254
|
+
expect(response.status).toBe(204);
|
|
255
|
+
expect(response.headers).toBeInstanceOf(Headers);
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it("should handle error response", async () => {
|
|
260
|
+
const fragment = defineFragment<{ apiKey: string }>("test");
|
|
261
|
+
const testFragment = createFragmentForTest(fragment, {
|
|
262
|
+
config: { apiKey: "test-key" },
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
const route = defineRoute({
|
|
266
|
+
method: "GET",
|
|
267
|
+
path: "/test",
|
|
268
|
+
errorCodes: ["NOT_FOUND"] as const,
|
|
269
|
+
handler: async (_ctx, { error }) => {
|
|
270
|
+
return error({ message: "Not found", code: "NOT_FOUND" }, 404);
|
|
271
|
+
},
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
const response = await testFragment.handler(route);
|
|
275
|
+
|
|
276
|
+
expect(response.type).toBe("error");
|
|
277
|
+
if (response.type === "error") {
|
|
278
|
+
expect(response.status).toBe(404);
|
|
279
|
+
expect(response.error).toEqual({ message: "Not found", code: "NOT_FOUND" });
|
|
280
|
+
expect(response.headers).toBeInstanceOf(Headers);
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it("should handle JSON stream response", async () => {
|
|
285
|
+
const fragment = defineFragment<{ apiKey: string }>("test");
|
|
286
|
+
const testFragment = createFragmentForTest(fragment, {
|
|
287
|
+
config: { apiKey: "test-key" },
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
const route = defineRoute({
|
|
291
|
+
method: "GET",
|
|
292
|
+
path: "/test/stream",
|
|
293
|
+
outputSchema: z.array(z.object({ value: z.number() })),
|
|
294
|
+
handler: async (_ctx, { jsonStream }) => {
|
|
295
|
+
return jsonStream(async (stream) => {
|
|
296
|
+
for (let i = 1; i <= 5; i++) {
|
|
297
|
+
await stream.write({ value: i });
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
},
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
const response = await testFragment.handler(route);
|
|
304
|
+
|
|
305
|
+
expect(response.type).toBe("jsonStream");
|
|
306
|
+
if (response.type === "jsonStream") {
|
|
307
|
+
expect(response.status).toBe(200);
|
|
308
|
+
expect(response.headers).toBeInstanceOf(Headers);
|
|
309
|
+
expect(response.headers.get("content-type")).toContain("application/x-ndjson");
|
|
310
|
+
|
|
311
|
+
const items = [];
|
|
312
|
+
for await (const item of response.stream) {
|
|
313
|
+
items.push(item);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
expect(items).toEqual([{ value: 1 }, { value: 2 }, { value: 3 }, { value: 4 }, { value: 5 }]);
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it("should handle path parameters", async () => {
|
|
321
|
+
const fragment = defineFragment<{}>("test");
|
|
322
|
+
const testFragment = createFragmentForTest(fragment, {
|
|
323
|
+
config: {},
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
const route = defineRoute({
|
|
327
|
+
method: "GET",
|
|
328
|
+
path: "/users/:id",
|
|
329
|
+
outputSchema: z.object({ userId: z.string() }),
|
|
330
|
+
handler: async ({ pathParams }, { json }) => {
|
|
331
|
+
return json({ userId: pathParams.id });
|
|
332
|
+
},
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
const response = await testFragment.handler(route, {
|
|
336
|
+
pathParams: { id: "123" },
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
expect(response.type).toBe("json");
|
|
340
|
+
if (response.type === "json") {
|
|
341
|
+
expect(response.data).toEqual({ userId: "123" });
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it("should handle query parameters", async () => {
|
|
346
|
+
const fragment = defineFragment<{}>("test");
|
|
347
|
+
const testFragment = createFragmentForTest(fragment, {
|
|
348
|
+
config: {},
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
const route = defineRoute({
|
|
352
|
+
method: "GET",
|
|
353
|
+
path: "/search",
|
|
354
|
+
outputSchema: z.object({ query: z.string() }),
|
|
355
|
+
handler: async ({ query }, { json }) => {
|
|
356
|
+
return json({ query: query.get("q") || "" });
|
|
357
|
+
},
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
const response = await testFragment.handler(route, {
|
|
361
|
+
query: { q: "test" },
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
expect(response.type).toBe("json");
|
|
365
|
+
if (response.type === "json") {
|
|
366
|
+
expect(response.data).toEqual({ query: "test" });
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it("should handle request body", async () => {
|
|
371
|
+
const fragment = defineFragment<{}>("test");
|
|
372
|
+
const testFragment = createFragmentForTest(fragment, {
|
|
373
|
+
config: {},
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
const route = defineRoute({
|
|
377
|
+
method: "POST",
|
|
378
|
+
path: "/users",
|
|
379
|
+
inputSchema: z.object({ name: z.string(), email: z.string() }),
|
|
380
|
+
outputSchema: z.object({ id: z.number(), name: z.string(), email: z.string() }),
|
|
381
|
+
handler: async ({ input }, { json }) => {
|
|
382
|
+
if (input) {
|
|
383
|
+
const data = await input.valid();
|
|
384
|
+
return json({ id: 1, name: data.name, email: data.email });
|
|
385
|
+
}
|
|
386
|
+
return json({ id: 1, name: "", email: "" });
|
|
387
|
+
},
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
const response = await testFragment.handler(route, {
|
|
391
|
+
body: { name: "John", email: "john@example.com" },
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
expect(response.type).toBe("json");
|
|
395
|
+
if (response.type === "json") {
|
|
396
|
+
expect(response.data).toEqual({ id: 1, name: "John", email: "john@example.com" });
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it("should handle custom headers", async () => {
|
|
401
|
+
const fragment = defineFragment<{}>("test");
|
|
402
|
+
const testFragment = createFragmentForTest(fragment, {
|
|
403
|
+
config: {},
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
const route = defineRoute({
|
|
407
|
+
method: "GET",
|
|
408
|
+
path: "/test",
|
|
409
|
+
outputSchema: z.object({ authHeader: z.string() }),
|
|
410
|
+
handler: async ({ headers }, { json }) => {
|
|
411
|
+
return json({ authHeader: headers.get("authorization") || "" });
|
|
412
|
+
},
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
const response = await testFragment.handler(route, {
|
|
416
|
+
headers: { authorization: "Bearer token" },
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
expect(response.type).toBe("json");
|
|
420
|
+
if (response.type === "json") {
|
|
421
|
+
expect(response.data).toEqual({ authHeader: "Bearer token" });
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
it("should properly type path params", async () => {
|
|
426
|
+
const fragment = defineFragment<{}>("test");
|
|
427
|
+
const testFragment = createFragmentForTest(fragment, {
|
|
428
|
+
config: {},
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
const route = defineRoute({
|
|
432
|
+
method: "GET",
|
|
433
|
+
path: "/orgs/:orgId/users/:userId",
|
|
434
|
+
outputSchema: z.object({ orgId: z.string(), userId: z.string() }),
|
|
435
|
+
handler: async ({ pathParams }, { json }) => {
|
|
436
|
+
return json({ orgId: pathParams.orgId, userId: pathParams.userId });
|
|
437
|
+
},
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
const response = await testFragment.handler(route, {
|
|
441
|
+
pathParams: { orgId: "123", userId: "456" },
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
expect(response.type).toBe("json");
|
|
445
|
+
if (response.type === "json") {
|
|
446
|
+
expect(response.data).toEqual({ orgId: "123", userId: "456" });
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
});
|