@fragno-dev/core 0.1.7 → 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 -64
- package/CHANGELOG.md +19 -0
- package/dist/api/api.d.ts +38 -2
- package/dist/api/api.d.ts.map +1 -0
- package/dist/api/api.js +9 -2
- 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-DngJDcmO.js → api/error.js} +2 -8
- 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 -3
- 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 -4
- package/dist/client/client.d.ts.map +1 -0
- package/dist/client/client.js +397 -6
- package/dist/client/client.js.map +1 -0
- package/dist/client/client.svelte.d.ts +5 -3
- package/dist/client/client.svelte.d.ts.map +1 -1
- package/dist/client/client.svelte.js +1 -5
- 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 -3
- package/dist/client/react.d.ts.map +1 -1
- package/dist/client/react.js +3 -5
- package/dist/client/react.js.map +1 -1
- package/dist/client/solid.d.ts +5 -3
- package/dist/client/solid.d.ts.map +1 -1
- package/dist/client/solid.js +2 -5
- package/dist/client/solid.js.map +1 -1
- package/dist/client/vanilla.d.ts +5 -3
- package/dist/client/vanilla.d.ts.map +1 -1
- package/dist/client/vanilla.js +2 -43
- package/dist/client/vanilla.js.map +1 -1
- package/dist/client/vue.d.ts +5 -3
- package/dist/client/vue.d.ts.map +1 -1
- package/dist/client/vue.js +1 -5
- 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 -4
- package/dist/mod.js +4 -6
- package/dist/request/request.d.ts +4 -0
- package/dist/request/request.js +5 -0
- package/dist/test/test.d.ts +62 -35
- package/dist/test/test.d.ts.map +1 -1
- package/dist/test/test.js +75 -40
- 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-BByDVfFD.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 +41 -6
- 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 +727 -0
- package/src/api/request-context-storage.ts +64 -0
- package/src/api/request-middleware.test.ts +301 -225
- package/src/api/route.test.ts +87 -1
- package/src/api/route.ts +345 -24
- 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 +26 -9
- package/src/request/request.ts +8 -0
- package/src/test/test.test.ts +200 -381
- package/src/test/test.ts +190 -117
- package/tsdown.config.ts +8 -5
- package/dist/api/fragment-builder.d.ts +0 -4
- package/dist/api/fragment-builder.js +0 -3
- package/dist/api/fragment-instantiation.d.ts +0 -4
- package/dist/api/fragment-instantiation.js +0 -6
- package/dist/api-BWN97TOr.d.ts +0 -377
- package/dist/api-BWN97TOr.d.ts.map +0 -1
- package/dist/api-DngJDcmO.js.map +0 -1
- package/dist/client-C5LsYHEI.js +0 -782
- package/dist/client-C5LsYHEI.js.map +0 -1
- package/dist/fragment-builder-DOnCVBqc.js +0 -47
- package/dist/fragment-builder-DOnCVBqc.js.map +0 -1
- package/dist/fragment-builder-MGr68GNb.d.ts +0 -409
- package/dist/fragment-builder-MGr68GNb.d.ts.map +0 -1
- package/dist/fragment-instantiation-C4wvwl6V.js +0 -446
- package/dist/fragment-instantiation-C4wvwl6V.js.map +0 -1
- package/dist/request-output-context-CdIjwmEN.js +0 -320
- package/dist/request-output-context-CdIjwmEN.js.map +0 -1
- package/dist/route-Bl9Zr1Yv.d.ts +0 -26
- package/dist/route-Bl9Zr1Yv.d.ts.map +0 -1
- package/dist/route-C5Uryylh.js +0 -21
- package/dist/route-C5Uryylh.js.map +0 -1
- package/dist/ssr-BByDVfFD.js.map +0 -1
- package/src/api/fragment-builder.ts +0 -80
- package/src/api/fragment-instantiation.test.ts +0 -460
- package/src/api/fragment-instantiation.ts +0 -499
- package/src/api/fragment.test.ts +0 -537
|
@@ -0,0 +1,1488 @@
|
|
|
1
|
+
import { describe, it, expect, vi, expectTypeOf } from "vitest";
|
|
2
|
+
import { defineFragment } from "./fragment-definition-builder";
|
|
3
|
+
import { instantiate, FragnoInstantiatedFragment } from "./fragment-instantiator";
|
|
4
|
+
import { defineRoute, defineRoutes, type AnyFragmentDefinition } from "./route";
|
|
5
|
+
import type { FragnoPublicConfig } from "./shared-types";
|
|
6
|
+
import type { RequestThisContext } from "./api";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
|
|
9
|
+
describe("fragment-instantiator", () => {
|
|
10
|
+
describe("basic instantiation", () => {
|
|
11
|
+
it("should instantiate a fragment with config and routes", () => {
|
|
12
|
+
interface Config {
|
|
13
|
+
apiKey: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const definition = defineFragment<Config>("test-fragment").build();
|
|
17
|
+
|
|
18
|
+
const route = defineRoute({
|
|
19
|
+
method: "GET",
|
|
20
|
+
path: "/hello",
|
|
21
|
+
handler: async (_input, { json }) => {
|
|
22
|
+
return json({ message: "hello" });
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const fragment = instantiate(definition)
|
|
27
|
+
.withConfig({ apiKey: "test-key" })
|
|
28
|
+
.withRoutes([route])
|
|
29
|
+
.withOptions({ mountRoute: "/api" })
|
|
30
|
+
.build();
|
|
31
|
+
|
|
32
|
+
expect(fragment).toBeInstanceOf(FragnoInstantiatedFragment);
|
|
33
|
+
expect(fragment.name).toBe("test-fragment");
|
|
34
|
+
expect(fragment.routes).toHaveLength(1);
|
|
35
|
+
expect(fragment.mountRoute).toBe("/api");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("should instantiate without config or routes", () => {
|
|
39
|
+
const definition = defineFragment("minimal-fragment").build();
|
|
40
|
+
|
|
41
|
+
const fragment = instantiate(definition).build();
|
|
42
|
+
|
|
43
|
+
expect(fragment).toBeInstanceOf(FragnoInstantiatedFragment);
|
|
44
|
+
expect(fragment.name).toBe("minimal-fragment");
|
|
45
|
+
expect(fragment.routes).toHaveLength(0);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe("dependencies", () => {
|
|
50
|
+
it("should call dependencies callback and expose deps", () => {
|
|
51
|
+
interface Config {
|
|
52
|
+
apiKey: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface Deps {
|
|
56
|
+
client: { apiKey: string };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const definition = defineFragment<Config, FragnoPublicConfig>(" test-fragment")
|
|
60
|
+
.withDependencies(
|
|
61
|
+
({ config }): Deps => ({
|
|
62
|
+
client: { apiKey: config.apiKey },
|
|
63
|
+
}),
|
|
64
|
+
)
|
|
65
|
+
.build();
|
|
66
|
+
|
|
67
|
+
const fragment = instantiate(definition)
|
|
68
|
+
.withConfig({ apiKey: "test-key" })
|
|
69
|
+
.withOptions({})
|
|
70
|
+
.build();
|
|
71
|
+
|
|
72
|
+
expect(fragment.$internal.deps).toEqual({
|
|
73
|
+
client: { apiKey: "test-key" },
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("should provide options to dependencies callback", () => {
|
|
78
|
+
interface Config {
|
|
79
|
+
value: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
interface Options extends FragnoPublicConfig {
|
|
83
|
+
customOption: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const definition = defineFragment<Config, Options>("test-fragment")
|
|
87
|
+
.withDependencies(({ config, options }) => ({
|
|
88
|
+
combined: `${config.value}-${options.customOption}`,
|
|
89
|
+
}))
|
|
90
|
+
.build();
|
|
91
|
+
|
|
92
|
+
const fragment = instantiate(definition)
|
|
93
|
+
.withConfig({ value: "config" })
|
|
94
|
+
.withOptions({ customOption: "option" })
|
|
95
|
+
.build();
|
|
96
|
+
|
|
97
|
+
expect(fragment.$internal.deps).toEqual({
|
|
98
|
+
combined: "config-option",
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe("base services", () => {
|
|
104
|
+
it("should call baseServices callback and expose services", () => {
|
|
105
|
+
interface Config {
|
|
106
|
+
prefix: string;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const definition = defineFragment<Config>("test-fragment")
|
|
110
|
+
.withDependencies(({ config }) => ({
|
|
111
|
+
prefix: config.prefix,
|
|
112
|
+
}))
|
|
113
|
+
.providesBaseService(({ deps }) => ({
|
|
114
|
+
greet: (name: string) => `${deps.prefix} ${name}`,
|
|
115
|
+
}))
|
|
116
|
+
.build();
|
|
117
|
+
|
|
118
|
+
const fragment = instantiate(definition)
|
|
119
|
+
.withConfig({ prefix: "Hello" })
|
|
120
|
+
.withOptions({})
|
|
121
|
+
.build();
|
|
122
|
+
|
|
123
|
+
expect(fragment.services.greet("World")).toBe("Hello World");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("should provide config, options, deps to baseServices", () => {
|
|
127
|
+
interface Config {
|
|
128
|
+
value: string;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const definition = defineFragment<Config>("test-fragment")
|
|
132
|
+
.withDependencies(({ config }) => ({
|
|
133
|
+
dep: config.value,
|
|
134
|
+
}))
|
|
135
|
+
.providesBaseService(({ config, deps }) => ({
|
|
136
|
+
getValue: () => `${config.value}-${deps.dep}`,
|
|
137
|
+
}))
|
|
138
|
+
.build();
|
|
139
|
+
|
|
140
|
+
const fragment = instantiate(definition)
|
|
141
|
+
.withConfig({ value: "test" })
|
|
142
|
+
.withOptions({})
|
|
143
|
+
.build();
|
|
144
|
+
|
|
145
|
+
expect(fragment.services.getValue()).toBe("test-test");
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe("named services", () => {
|
|
150
|
+
it("should call namedServices factories and expose services", () => {
|
|
151
|
+
const definition = defineFragment("test-fragment")
|
|
152
|
+
.providesService("mathService", () => ({
|
|
153
|
+
add: (a: number, b: number) => a + b,
|
|
154
|
+
multiply: (a: number, b: number) => a * b,
|
|
155
|
+
}))
|
|
156
|
+
.build();
|
|
157
|
+
|
|
158
|
+
const fragment = instantiate(definition).withOptions({}).build();
|
|
159
|
+
|
|
160
|
+
expect(fragment.services.mathService.add(2, 3)).toBe(5);
|
|
161
|
+
expect(fragment.services.mathService.multiply(4, 5)).toBe(20);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("should provide context to named service factories", () => {
|
|
165
|
+
interface Config {
|
|
166
|
+
multiplier: number;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const definition = defineFragment<Config>("test-fragment")
|
|
170
|
+
.withDependencies(({ config }) => ({
|
|
171
|
+
multiplier: config.multiplier,
|
|
172
|
+
}))
|
|
173
|
+
.providesService("mathService", ({ deps }) => ({
|
|
174
|
+
scale: (value: number) => value * deps.multiplier,
|
|
175
|
+
}))
|
|
176
|
+
.build();
|
|
177
|
+
|
|
178
|
+
const fragment = instantiate(definition)
|
|
179
|
+
.withConfig({ multiplier: 10 })
|
|
180
|
+
.withOptions({})
|
|
181
|
+
.build();
|
|
182
|
+
|
|
183
|
+
expect(fragment.services.mathService.scale(5)).toBe(50);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("should merge base services and named services", () => {
|
|
187
|
+
const definition = defineFragment("test-fragment")
|
|
188
|
+
.providesBaseService(() => ({
|
|
189
|
+
baseMethod: () => "base",
|
|
190
|
+
}))
|
|
191
|
+
.providesService("namedService", () => ({
|
|
192
|
+
namedMethod: () => "named",
|
|
193
|
+
}))
|
|
194
|
+
.build();
|
|
195
|
+
|
|
196
|
+
const fragment = instantiate(definition).withOptions({}).build();
|
|
197
|
+
|
|
198
|
+
expect(fragment.services.baseMethod()).toBe("base");
|
|
199
|
+
expect(fragment.services.namedService.namedMethod()).toBe("named");
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
describe("service dependencies", () => {
|
|
204
|
+
it("should validate required service dependencies", () => {
|
|
205
|
+
interface EmailService {
|
|
206
|
+
send: (to: string) => Promise<void>;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const definition = defineFragment("test-fragment")
|
|
210
|
+
.usesService<"emailService", EmailService>("emailService")
|
|
211
|
+
.build();
|
|
212
|
+
|
|
213
|
+
expect(() => {
|
|
214
|
+
instantiate(definition).withOptions({}).build();
|
|
215
|
+
}).toThrow("Fragment 'test-fragment' requires service 'emailService'");
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("should accept provided service dependencies", () => {
|
|
219
|
+
interface EmailService {
|
|
220
|
+
send: (to: string) => Promise<void>;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const emailService: EmailService = {
|
|
224
|
+
send: async (to: string) => {
|
|
225
|
+
console.log(`Sending email to ${to}`);
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const definition = defineFragment("test-fragment")
|
|
230
|
+
.usesService<"emailService", EmailService>("emailService")
|
|
231
|
+
.providesBaseService(({ serviceDeps }) => ({
|
|
232
|
+
sendWelcomeEmail: async (to: string) => {
|
|
233
|
+
await serviceDeps.emailService.send(to);
|
|
234
|
+
},
|
|
235
|
+
}))
|
|
236
|
+
.build();
|
|
237
|
+
|
|
238
|
+
const fragment = instantiate(definition)
|
|
239
|
+
.withOptions({})
|
|
240
|
+
.withServices({ emailService })
|
|
241
|
+
.build();
|
|
242
|
+
|
|
243
|
+
expect(fragment.services.sendWelcomeEmail).toBeDefined();
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it("should provide serviceDeps to base services", () => {
|
|
247
|
+
interface AuthService {
|
|
248
|
+
getCurrentUser: () => { id: string; name: string };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const authService: AuthService = {
|
|
252
|
+
getCurrentUser: () => ({ id: "123", name: "John" }),
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
const definition = defineFragment("test-fragment")
|
|
256
|
+
.usesService<"authService", AuthService>("authService")
|
|
257
|
+
.providesBaseService(({ serviceDeps }) => ({
|
|
258
|
+
getUserName: () => serviceDeps.authService.getCurrentUser().name,
|
|
259
|
+
}))
|
|
260
|
+
.build();
|
|
261
|
+
|
|
262
|
+
const fragment = instantiate(definition)
|
|
263
|
+
.withOptions({})
|
|
264
|
+
.withServices({ authService })
|
|
265
|
+
.build();
|
|
266
|
+
|
|
267
|
+
expect(fragment.services.getUserName()).toBe("John");
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it("should provide serviceDeps to named services", () => {
|
|
271
|
+
interface AuthService {
|
|
272
|
+
getCurrentUser: () => { id: string };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const authService: AuthService = {
|
|
276
|
+
getCurrentUser: () => ({ id: "123" }),
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
const definition = defineFragment("test-fragment")
|
|
280
|
+
.usesService<"authService", AuthService>("authService")
|
|
281
|
+
.providesService("userService", ({ serviceDeps }) => ({
|
|
282
|
+
getUserId: () => serviceDeps.authService.getCurrentUser().id,
|
|
283
|
+
}))
|
|
284
|
+
.build();
|
|
285
|
+
|
|
286
|
+
const fragment = instantiate(definition)
|
|
287
|
+
.withOptions({})
|
|
288
|
+
.withServices({ authService })
|
|
289
|
+
.build();
|
|
290
|
+
|
|
291
|
+
expect(fragment.services.userService.getUserId()).toBe("123");
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it("should combine uses, provides, and base services", () => {
|
|
295
|
+
interface AuthService {
|
|
296
|
+
getCurrentUser: () => { id: string };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const authService: AuthService = {
|
|
300
|
+
getCurrentUser: () => ({ id: "123" }),
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
const definition = defineFragment("test-fragment")
|
|
304
|
+
.usesService<"authService", AuthService>("authService")
|
|
305
|
+
.providesService("userService", ({ serviceDeps }) => ({
|
|
306
|
+
getUserId: () => serviceDeps.authService.getCurrentUser().id,
|
|
307
|
+
}))
|
|
308
|
+
.providesBaseService(({ serviceDeps }) => ({
|
|
309
|
+
getNextUserId: () => serviceDeps.authService.getCurrentUser().id + 1,
|
|
310
|
+
}))
|
|
311
|
+
.build();
|
|
312
|
+
|
|
313
|
+
const fragment = instantiate(definition)
|
|
314
|
+
.withOptions({})
|
|
315
|
+
.withServices({ authService })
|
|
316
|
+
.build();
|
|
317
|
+
|
|
318
|
+
const services = fragment.services;
|
|
319
|
+
expectTypeOf(services).toMatchObjectType<{
|
|
320
|
+
getNextUserId: () => string;
|
|
321
|
+
userService: { getUserId: () => string };
|
|
322
|
+
}>();
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
describe("handler execution", () => {
|
|
327
|
+
it("should execute route handlers via handler method", async () => {
|
|
328
|
+
const definition = defineFragment("test-fragment").build();
|
|
329
|
+
|
|
330
|
+
const route = defineRoute({
|
|
331
|
+
method: "GET",
|
|
332
|
+
path: "/hello",
|
|
333
|
+
handler: async (_input, { json }) => {
|
|
334
|
+
return json({ message: "Hello World" });
|
|
335
|
+
},
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
const fragment = instantiate(definition)
|
|
339
|
+
.withRoutes([route])
|
|
340
|
+
.withOptions({ mountRoute: "/api" })
|
|
341
|
+
.build();
|
|
342
|
+
|
|
343
|
+
const request = new Request("http://localhost/api/hello");
|
|
344
|
+
const response = await fragment.handler(request);
|
|
345
|
+
|
|
346
|
+
expect(response.status).toBe(200);
|
|
347
|
+
const data = await response.json();
|
|
348
|
+
expect(data).toEqual({ message: "Hello World" });
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it("should return 404 for unknown routes", async () => {
|
|
352
|
+
const definition = defineFragment("test-fragment").build();
|
|
353
|
+
|
|
354
|
+
const fragment = instantiate(definition)
|
|
355
|
+
.withRoutes([])
|
|
356
|
+
.withOptions({ mountRoute: "/api" })
|
|
357
|
+
.build();
|
|
358
|
+
|
|
359
|
+
const request = new Request("http://localhost/api/unknown");
|
|
360
|
+
const response = await fragment.handler(request);
|
|
361
|
+
|
|
362
|
+
expect(response.status).toBe(404);
|
|
363
|
+
const data = await response.json();
|
|
364
|
+
expect(data.code).toBe("ROUTE_NOT_FOUND");
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
it("should return 404 for wrong mount route", async () => {
|
|
368
|
+
const definition = defineFragment("test-fragment").build();
|
|
369
|
+
|
|
370
|
+
const route = defineRoute({
|
|
371
|
+
method: "GET",
|
|
372
|
+
path: "/hello",
|
|
373
|
+
handler: async (_input, { json }) => {
|
|
374
|
+
return json({ message: "Hello" });
|
|
375
|
+
},
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
const fragment = instantiate(definition)
|
|
379
|
+
.withRoutes([route])
|
|
380
|
+
.withOptions({ mountRoute: "/api" })
|
|
381
|
+
.build();
|
|
382
|
+
|
|
383
|
+
const request = new Request("http://localhost/wrong/hello");
|
|
384
|
+
const response = await fragment.handler(request);
|
|
385
|
+
|
|
386
|
+
expect(response.status).toBe(404);
|
|
387
|
+
const data = await response.json();
|
|
388
|
+
expect(data.code).toBe("ROUTE_NOT_FOUND");
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it("should handle route with path params", async () => {
|
|
392
|
+
const definition = defineFragment("test-fragment").build();
|
|
393
|
+
|
|
394
|
+
const route = defineRoute({
|
|
395
|
+
method: "GET",
|
|
396
|
+
path: "/users/:id",
|
|
397
|
+
handler: async (input, { json }) => {
|
|
398
|
+
return json({ userId: input.pathParams.id });
|
|
399
|
+
},
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
const fragment = instantiate(definition)
|
|
403
|
+
.withRoutes([route])
|
|
404
|
+
.withOptions({ mountRoute: "/api" })
|
|
405
|
+
.build();
|
|
406
|
+
|
|
407
|
+
const request = new Request("http://localhost/api/users/123");
|
|
408
|
+
const response = await fragment.handler(request);
|
|
409
|
+
|
|
410
|
+
expect(response.status).toBe(200);
|
|
411
|
+
const data = await response.json();
|
|
412
|
+
expect(data).toEqual({ userId: "123" });
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
it("should handle POST requests with body", async () => {
|
|
416
|
+
const definition = defineFragment("test-fragment").build();
|
|
417
|
+
|
|
418
|
+
const inputSchema = z.object({
|
|
419
|
+
test: z.string(),
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
const route = defineRoute({
|
|
423
|
+
method: "POST",
|
|
424
|
+
path: "/data",
|
|
425
|
+
inputSchema,
|
|
426
|
+
handler: async ({ input }, { json }) => {
|
|
427
|
+
const body = await input.valid();
|
|
428
|
+
return json({ received: body });
|
|
429
|
+
},
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
const fragment = instantiate(definition)
|
|
433
|
+
.withRoutes([route])
|
|
434
|
+
.withOptions({ mountRoute: "/api" })
|
|
435
|
+
.build();
|
|
436
|
+
|
|
437
|
+
const request = new Request("http://localhost/api/data", {
|
|
438
|
+
method: "POST",
|
|
439
|
+
headers: { "Content-Type": "application/json" },
|
|
440
|
+
body: JSON.stringify({ test: "data" }),
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
const response = await fragment.handler(request);
|
|
444
|
+
|
|
445
|
+
expect(response.status).toBe(200);
|
|
446
|
+
const data = await response.json();
|
|
447
|
+
expect(data).toEqual({ received: { test: "data" } });
|
|
448
|
+
});
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
describe("callRoute", () => {
|
|
452
|
+
it("should call route directly with callRoute", async () => {
|
|
453
|
+
const definition = defineFragment("test-fragment").build();
|
|
454
|
+
|
|
455
|
+
const route = defineRoute({
|
|
456
|
+
method: "GET",
|
|
457
|
+
path: "/hello",
|
|
458
|
+
handler: async (_input, { json }) => {
|
|
459
|
+
return json({ message: "Hello from callRoute" });
|
|
460
|
+
},
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
const fragment = instantiate(definition).withRoutes([route]).withOptions({}).build();
|
|
464
|
+
|
|
465
|
+
const response = await fragment.callRoute("GET", "/hello");
|
|
466
|
+
|
|
467
|
+
expect(response.type).toBe("json");
|
|
468
|
+
if (response.type === "json") {
|
|
469
|
+
expect(response.data).toEqual({ message: "Hello from callRoute" });
|
|
470
|
+
}
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
it("should pass path params to callRoute", async () => {
|
|
474
|
+
const definition = defineFragment("test-fragment").build();
|
|
475
|
+
|
|
476
|
+
const route = defineRoute({
|
|
477
|
+
method: "GET",
|
|
478
|
+
path: "/users/:id",
|
|
479
|
+
handler: async (input, { json }) => {
|
|
480
|
+
return json({ userId: input.pathParams.id });
|
|
481
|
+
},
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
const fragment = instantiate(definition).withRoutes([route]).withOptions({}).build();
|
|
485
|
+
|
|
486
|
+
const response = await fragment.callRoute("GET", "/users/:id", {
|
|
487
|
+
pathParams: { id: "456" },
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
expect(response.type).toBe("json");
|
|
491
|
+
if (response.type === "json") {
|
|
492
|
+
expect(response.data).toEqual({ userId: "456" });
|
|
493
|
+
}
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
it("should pass body to callRoute", async () => {
|
|
497
|
+
const definition = defineFragment("test-fragment").build();
|
|
498
|
+
|
|
499
|
+
const inputSchema = z.object({
|
|
500
|
+
test: z.string(),
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
const route = defineRoute({
|
|
504
|
+
method: "POST",
|
|
505
|
+
path: "/data",
|
|
506
|
+
inputSchema,
|
|
507
|
+
handler: async ({ input }, { json }) => {
|
|
508
|
+
const body = await input.valid();
|
|
509
|
+
return json({ received: body });
|
|
510
|
+
},
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
const fragment = instantiate(definition).withRoutes([route]).withOptions({}).build();
|
|
514
|
+
|
|
515
|
+
const response = await fragment.callRoute("POST", "/data", {
|
|
516
|
+
body: { test: "data" },
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
expect(response.type).toBe("json");
|
|
520
|
+
if (response.type === "json") {
|
|
521
|
+
expect(response.data).toEqual({ received: { test: "data" } });
|
|
522
|
+
}
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
it("should return error for unknown route in callRoute", async () => {
|
|
526
|
+
const definition = defineFragment("test-fragment").build();
|
|
527
|
+
|
|
528
|
+
const route = defineRoute({
|
|
529
|
+
method: "GET",
|
|
530
|
+
path: "/exists",
|
|
531
|
+
handler: async (_input, { json }) => {
|
|
532
|
+
return json({ message: "exists" });
|
|
533
|
+
},
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
const fragment = instantiate(definition).withRoutes([route]).withOptions({}).build();
|
|
537
|
+
|
|
538
|
+
// @ts-expect-error - /unknown is not a valid route
|
|
539
|
+
const response = await fragment.callRouteRaw("GET", "/unknown");
|
|
540
|
+
|
|
541
|
+
expect(response.status).toBe(404);
|
|
542
|
+
const data = await response.json();
|
|
543
|
+
expect(data.code).toBe("ROUTE_NOT_FOUND");
|
|
544
|
+
});
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
describe("callRouteRaw", () => {
|
|
548
|
+
it("should call route and return raw Response", async () => {
|
|
549
|
+
const definition = defineFragment("test-fragment").build();
|
|
550
|
+
|
|
551
|
+
const route = defineRoute({
|
|
552
|
+
method: "GET",
|
|
553
|
+
path: "/hello",
|
|
554
|
+
handler: async (_input, { json }) => {
|
|
555
|
+
return json({ message: "Raw response" });
|
|
556
|
+
},
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
const fragment = instantiate(definition).withRoutes([route]).withOptions({}).build();
|
|
560
|
+
|
|
561
|
+
const response = await fragment.callRouteRaw("GET", "/hello");
|
|
562
|
+
|
|
563
|
+
expect(response).toBeInstanceOf(Response);
|
|
564
|
+
expect(response.status).toBe(200);
|
|
565
|
+
const data = await response.json();
|
|
566
|
+
expect(data).toEqual({ message: "Raw response" });
|
|
567
|
+
});
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
describe("middleware", () => {
|
|
571
|
+
it("should execute middleware before handler", async () => {
|
|
572
|
+
const definition = defineFragment("test-fragment").build();
|
|
573
|
+
|
|
574
|
+
const route = defineRoute({
|
|
575
|
+
method: "GET",
|
|
576
|
+
path: "/hello",
|
|
577
|
+
handler: async (_input, { json }) => {
|
|
578
|
+
return json({ message: "Hello" });
|
|
579
|
+
},
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
const fragment = instantiate(definition)
|
|
583
|
+
.withRoutes([route])
|
|
584
|
+
.withOptions({ mountRoute: "/api" })
|
|
585
|
+
.build();
|
|
586
|
+
|
|
587
|
+
const middlewareSpy = vi.fn();
|
|
588
|
+
|
|
589
|
+
fragment.withMiddleware(async (input, _output) => {
|
|
590
|
+
middlewareSpy(input.path);
|
|
591
|
+
// Don't return anything to allow request to continue
|
|
592
|
+
return undefined;
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
const request = new Request("http://localhost/api/hello");
|
|
596
|
+
await fragment.handler(request);
|
|
597
|
+
|
|
598
|
+
expect(middlewareSpy).toHaveBeenCalledWith("/hello");
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
it("should short-circuit with middleware response", async () => {
|
|
602
|
+
const definition = defineFragment("test-fragment").build();
|
|
603
|
+
|
|
604
|
+
const route = defineRoute({
|
|
605
|
+
method: "GET",
|
|
606
|
+
path: "/hello",
|
|
607
|
+
handler: async (_input, { json }) => {
|
|
608
|
+
return json({ message: "From handler" });
|
|
609
|
+
},
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
const fragment = instantiate(definition)
|
|
613
|
+
.withRoutes([route])
|
|
614
|
+
.withOptions({ mountRoute: "/api" })
|
|
615
|
+
.build();
|
|
616
|
+
|
|
617
|
+
fragment.withMiddleware(async () => {
|
|
618
|
+
return Response.json({ message: "From middleware" });
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
const request = new Request("http://localhost/api/hello");
|
|
622
|
+
const response = await fragment.handler(request);
|
|
623
|
+
|
|
624
|
+
const data = await response.json();
|
|
625
|
+
expect(data).toEqual({ message: "From middleware" });
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
it("should throw error when setting middleware twice", () => {
|
|
629
|
+
const definition = defineFragment("test-fragment").build();
|
|
630
|
+
|
|
631
|
+
const fragment = instantiate(definition).withOptions({}).build();
|
|
632
|
+
|
|
633
|
+
fragment.withMiddleware(async () => {
|
|
634
|
+
return undefined;
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
expect(() => {
|
|
638
|
+
fragment.withMiddleware(async () => {
|
|
639
|
+
return undefined;
|
|
640
|
+
});
|
|
641
|
+
}).toThrow("Middleware already set");
|
|
642
|
+
});
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
describe("handlersFor", () => {
|
|
646
|
+
it("should generate astro handlers", () => {
|
|
647
|
+
const definition = defineFragment("test-fragment").build();
|
|
648
|
+
|
|
649
|
+
const fragment = instantiate(definition).withRoutes([]).withOptions({}).build();
|
|
650
|
+
|
|
651
|
+
const handlers = fragment.handlersFor("astro");
|
|
652
|
+
|
|
653
|
+
expect(handlers).toHaveProperty("ALL");
|
|
654
|
+
expect(typeof handlers.ALL).toBe("function");
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
it("should generate react-router handlers", () => {
|
|
658
|
+
const definition = defineFragment("test-fragment").build();
|
|
659
|
+
|
|
660
|
+
const fragment = instantiate(definition).withRoutes([]).withOptions({}).build();
|
|
661
|
+
|
|
662
|
+
const handlers = fragment.handlersFor("react-router");
|
|
663
|
+
|
|
664
|
+
expect(handlers).toHaveProperty("loader");
|
|
665
|
+
expect(handlers).toHaveProperty("action");
|
|
666
|
+
expect(typeof handlers.loader).toBe("function");
|
|
667
|
+
expect(typeof handlers.action).toBe("function");
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
it("should generate next-js handlers", () => {
|
|
671
|
+
const definition = defineFragment("test-fragment").build();
|
|
672
|
+
|
|
673
|
+
const fragment = instantiate(definition).withRoutes([]).withOptions({}).build();
|
|
674
|
+
|
|
675
|
+
const handlers = fragment.handlersFor("next-js");
|
|
676
|
+
|
|
677
|
+
expect(handlers).toHaveProperty("GET");
|
|
678
|
+
expect(handlers).toHaveProperty("POST");
|
|
679
|
+
expect(handlers).toHaveProperty("PUT");
|
|
680
|
+
expect(handlers).toHaveProperty("DELETE");
|
|
681
|
+
expect(handlers).toHaveProperty("PATCH");
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
it("should generate svelte-kit handlers", () => {
|
|
685
|
+
const definition = defineFragment("test-fragment").build();
|
|
686
|
+
|
|
687
|
+
const fragment = instantiate(definition).withRoutes([]).withOptions({}).build();
|
|
688
|
+
|
|
689
|
+
const handlers = fragment.handlersFor("svelte-kit");
|
|
690
|
+
|
|
691
|
+
expect(handlers).toHaveProperty("GET");
|
|
692
|
+
expect(handlers).toHaveProperty("POST");
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
it("should generate solid-start handlers", () => {
|
|
696
|
+
const definition = defineFragment("test-fragment").build();
|
|
697
|
+
|
|
698
|
+
const fragment = instantiate(definition).withRoutes([]).withOptions({}).build();
|
|
699
|
+
|
|
700
|
+
const handlers = fragment.handlersFor("solid-start");
|
|
701
|
+
|
|
702
|
+
expect(handlers).toHaveProperty("GET");
|
|
703
|
+
expect(handlers).toHaveProperty("POST");
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
it("should generate tanstack-start handlers", () => {
|
|
707
|
+
const definition = defineFragment("test-fragment").build();
|
|
708
|
+
|
|
709
|
+
const fragment = instantiate(definition).withRoutes([]).withOptions({}).build();
|
|
710
|
+
|
|
711
|
+
const handlers = fragment.handlersFor("tanstack-start");
|
|
712
|
+
|
|
713
|
+
expect(handlers).toHaveProperty("GET");
|
|
714
|
+
expect(handlers).toHaveProperty("POST");
|
|
715
|
+
});
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
describe("request context", () => {
|
|
719
|
+
it("should use custom thisContext from createThisContext", async () => {
|
|
720
|
+
interface CustomThisContext extends RequestThisContext {
|
|
721
|
+
customMethod: () => string;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
const definition = defineFragment("test-fragment").build();
|
|
725
|
+
|
|
726
|
+
// Manually add createThisContext to definition
|
|
727
|
+
const definitionWithContext = {
|
|
728
|
+
...definition,
|
|
729
|
+
createThisContext: () => {
|
|
730
|
+
const ctx = {
|
|
731
|
+
customMethod: () => "custom value",
|
|
732
|
+
};
|
|
733
|
+
return { serviceContext: ctx, handlerContext: ctx };
|
|
734
|
+
},
|
|
735
|
+
} satisfies AnyFragmentDefinition;
|
|
736
|
+
|
|
737
|
+
const route = defineRoute({
|
|
738
|
+
method: "GET",
|
|
739
|
+
path: "/test",
|
|
740
|
+
handler: async function (this: CustomThisContext, _input, { json }) {
|
|
741
|
+
return json({ value: this.customMethod() });
|
|
742
|
+
},
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
const fragment = instantiate(definitionWithContext)
|
|
746
|
+
.withRoutes([route])
|
|
747
|
+
.withOptions({ mountRoute: "/api" })
|
|
748
|
+
.build();
|
|
749
|
+
|
|
750
|
+
const request = new Request("http://localhost/api/test");
|
|
751
|
+
const response = await fragment.handler(request);
|
|
752
|
+
|
|
753
|
+
const data = await response.json();
|
|
754
|
+
expect(data).toEqual({ value: "custom value" });
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
it("should create fresh context per request", async () => {
|
|
758
|
+
const contextCreationSpy = vi.fn();
|
|
759
|
+
|
|
760
|
+
const definition = defineFragment("test-fragment")
|
|
761
|
+
.withRequestStorage(() => ({ counter: 0 }))
|
|
762
|
+
.withThisContext(({ storage }) => {
|
|
763
|
+
const ctx = {
|
|
764
|
+
get requestId() {
|
|
765
|
+
contextCreationSpy();
|
|
766
|
+
return { text: "default", counter: ++storage.getStore().counter };
|
|
767
|
+
},
|
|
768
|
+
};
|
|
769
|
+
return { serviceContext: ctx, handlerContext: ctx };
|
|
770
|
+
})
|
|
771
|
+
.build();
|
|
772
|
+
|
|
773
|
+
const routes = defineRoutes(definition).create(({ defineRoute }) => {
|
|
774
|
+
return [
|
|
775
|
+
defineRoute({
|
|
776
|
+
method: "GET",
|
|
777
|
+
path: "/test",
|
|
778
|
+
handler: async function (_input, { json }) {
|
|
779
|
+
return json({ requestId: this.requestId });
|
|
780
|
+
},
|
|
781
|
+
}),
|
|
782
|
+
];
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
const fragment = instantiate(definition)
|
|
786
|
+
.withRoutes([routes])
|
|
787
|
+
.withOptions({ mountRoute: "/api" })
|
|
788
|
+
.build();
|
|
789
|
+
|
|
790
|
+
const request = new Request("http://localhost/api/test");
|
|
791
|
+
const response = await fragment.handler(request);
|
|
792
|
+
expect(contextCreationSpy).toHaveBeenCalled();
|
|
793
|
+
const data = await response.json();
|
|
794
|
+
expect(data).toEqual({ requestId: { text: "default", counter: 1 } });
|
|
795
|
+
|
|
796
|
+
const response2 = await fragment.handler(request);
|
|
797
|
+
const data2 = await response2.json();
|
|
798
|
+
expect(data2).toEqual({ requestId: { text: "default", counter: 1 } });
|
|
799
|
+
|
|
800
|
+
expect(contextCreationSpy).toHaveBeenCalledTimes(2);
|
|
801
|
+
});
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
describe("defineService with custom this context", () => {
|
|
805
|
+
it("withThisContext types", () => {
|
|
806
|
+
const definition = defineFragment("test-fragment")
|
|
807
|
+
.withDependencies(() => ({ apiKey: "key", number: 5 }))
|
|
808
|
+
.withThisContext(() => {
|
|
809
|
+
const ctx = {
|
|
810
|
+
x: 3,
|
|
811
|
+
y: "hello",
|
|
812
|
+
get someNumber() {
|
|
813
|
+
return 5;
|
|
814
|
+
},
|
|
815
|
+
};
|
|
816
|
+
return { serviceContext: ctx, handlerContext: ctx };
|
|
817
|
+
})
|
|
818
|
+
.providesBaseService(({ deps, defineService }) =>
|
|
819
|
+
defineService({
|
|
820
|
+
method1: function () {
|
|
821
|
+
expectTypeOf(this).toMatchObjectType<{
|
|
822
|
+
x: number;
|
|
823
|
+
y: string;
|
|
824
|
+
readonly someNumber: number;
|
|
825
|
+
}>();
|
|
826
|
+
|
|
827
|
+
return `${deps.apiKey}-${this.someNumber}`;
|
|
828
|
+
},
|
|
829
|
+
}),
|
|
830
|
+
)
|
|
831
|
+
.build();
|
|
832
|
+
|
|
833
|
+
// Type check: definition should have correct context factory
|
|
834
|
+
expect(definition.createThisContext).toBeDefined();
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
it("withRequestStorage allows mutating stored data", async () => {
|
|
838
|
+
const definition = defineFragment("test-fragment")
|
|
839
|
+
.withDependencies(() => ({ startingCounter: 5 }))
|
|
840
|
+
.withRequestStorage(({ deps }) => ({ counter: deps.startingCounter }))
|
|
841
|
+
.withThisContext(({ storage }) => {
|
|
842
|
+
const ctx = {
|
|
843
|
+
// Getter to access current counter value from storage
|
|
844
|
+
get counter() {
|
|
845
|
+
return storage.getStore().counter;
|
|
846
|
+
},
|
|
847
|
+
// Method to increment the counter in storage
|
|
848
|
+
incrementCounter() {
|
|
849
|
+
storage.getStore().counter++;
|
|
850
|
+
},
|
|
851
|
+
};
|
|
852
|
+
return { serviceContext: ctx, handlerContext: ctx };
|
|
853
|
+
})
|
|
854
|
+
.providesBaseService(({ defineService }) =>
|
|
855
|
+
defineService({
|
|
856
|
+
// Service that reads the counter
|
|
857
|
+
getCounter: function () {
|
|
858
|
+
return this.counter;
|
|
859
|
+
},
|
|
860
|
+
// Service that increments the stored counter
|
|
861
|
+
incrementCounter: function () {
|
|
862
|
+
this.incrementCounter();
|
|
863
|
+
return this.counter;
|
|
864
|
+
},
|
|
865
|
+
}),
|
|
866
|
+
)
|
|
867
|
+
.build();
|
|
868
|
+
|
|
869
|
+
const routes = defineRoutes(definition).create(({ services, defineRoute }) => [
|
|
870
|
+
defineRoute({
|
|
871
|
+
method: "GET",
|
|
872
|
+
path: "/test-counter",
|
|
873
|
+
handler: async function (_input, { json }) {
|
|
874
|
+
// Type check: this should have counter property and incrementCounter method
|
|
875
|
+
expectTypeOf(this).toMatchObjectType<{
|
|
876
|
+
readonly counter: number;
|
|
877
|
+
incrementCounter: () => void;
|
|
878
|
+
}>();
|
|
879
|
+
|
|
880
|
+
// Read initial counter (starts at 0)
|
|
881
|
+
const initial = services.getCounter();
|
|
882
|
+
|
|
883
|
+
// Increment through service - this mutates the storage
|
|
884
|
+
const afterFirst = services.incrementCounter();
|
|
885
|
+
const afterSecond = services.incrementCounter();
|
|
886
|
+
|
|
887
|
+
// Direct access via this also shows the mutated value
|
|
888
|
+
const thisCounter = this.counter;
|
|
889
|
+
|
|
890
|
+
// Increment via this context directly
|
|
891
|
+
this.incrementCounter();
|
|
892
|
+
const afterDirect = this.counter;
|
|
893
|
+
|
|
894
|
+
return json({
|
|
895
|
+
initial,
|
|
896
|
+
afterFirst,
|
|
897
|
+
afterSecond,
|
|
898
|
+
thisCounter,
|
|
899
|
+
afterDirect,
|
|
900
|
+
});
|
|
901
|
+
},
|
|
902
|
+
}),
|
|
903
|
+
]);
|
|
904
|
+
|
|
905
|
+
const fragment = instantiate(definition)
|
|
906
|
+
.withRoutes([routes])
|
|
907
|
+
.withOptions({ mountRoute: "/api" })
|
|
908
|
+
.build();
|
|
909
|
+
|
|
910
|
+
const response = await fragment.handler(new Request("http://localhost/api/test-counter"));
|
|
911
|
+
const data = await response.json();
|
|
912
|
+
|
|
913
|
+
// The counter increments are persisted in storage throughout the request
|
|
914
|
+
expect(data).toEqual({
|
|
915
|
+
initial: 5,
|
|
916
|
+
afterFirst: 6,
|
|
917
|
+
afterSecond: 7,
|
|
918
|
+
thisCounter: 7,
|
|
919
|
+
afterDirect: 8,
|
|
920
|
+
});
|
|
921
|
+
|
|
922
|
+
expect(response.status).toBe(200);
|
|
923
|
+
});
|
|
924
|
+
|
|
925
|
+
it("should allow services to use custom this context at runtime", async () => {
|
|
926
|
+
const definition = defineFragment("test-fragment")
|
|
927
|
+
.withDependencies(() => ({ apiKey: "key", number: 5 }))
|
|
928
|
+
.withThisContext(({ deps }) => {
|
|
929
|
+
const ctx = {
|
|
930
|
+
get someNumber() {
|
|
931
|
+
return deps.number;
|
|
932
|
+
},
|
|
933
|
+
};
|
|
934
|
+
return { serviceContext: ctx, handlerContext: ctx };
|
|
935
|
+
})
|
|
936
|
+
.providesBaseService(({ deps, defineService }) =>
|
|
937
|
+
defineService({
|
|
938
|
+
method1: function () {
|
|
939
|
+
expectTypeOf(this).toMatchObjectType<{
|
|
940
|
+
readonly someNumber: number;
|
|
941
|
+
}>();
|
|
942
|
+
|
|
943
|
+
return `${deps.apiKey}-${this.someNumber}`;
|
|
944
|
+
},
|
|
945
|
+
}),
|
|
946
|
+
)
|
|
947
|
+
.build();
|
|
948
|
+
|
|
949
|
+
const fragment = instantiate(definition).withOptions({}).build();
|
|
950
|
+
|
|
951
|
+
expect(fragment.services.method1).toBeDefined();
|
|
952
|
+
|
|
953
|
+
// Calling the service method outside of a request context will not work properly
|
|
954
|
+
const result1 = fragment.services.method1();
|
|
955
|
+
expect(result1).toBe("key-5");
|
|
956
|
+
|
|
957
|
+
// Now let's use defineRoutes to access services properly in routes
|
|
958
|
+
const routesFactory = defineRoutes(definition).create(({ services, defineRoute }) => {
|
|
959
|
+
return [
|
|
960
|
+
defineRoute({
|
|
961
|
+
method: "GET",
|
|
962
|
+
path: "/test-service",
|
|
963
|
+
handler: async function (_input, { json }) {
|
|
964
|
+
expectTypeOf(this).toMatchObjectType<{
|
|
965
|
+
readonly someNumber: number;
|
|
966
|
+
}>();
|
|
967
|
+
|
|
968
|
+
expect(this).toMatchObject({
|
|
969
|
+
someNumber: 5,
|
|
970
|
+
});
|
|
971
|
+
|
|
972
|
+
// Services are available from the route factory context!
|
|
973
|
+
const serviceResult = services.method1();
|
|
974
|
+
return json({ result: serviceResult + "-" + (this.someNumber + 1) });
|
|
975
|
+
},
|
|
976
|
+
}),
|
|
977
|
+
];
|
|
978
|
+
});
|
|
979
|
+
|
|
980
|
+
const fragmentWithRoute = instantiate(definition)
|
|
981
|
+
.withRoutes([routesFactory])
|
|
982
|
+
.withOptions({ mountRoute: "/api" })
|
|
983
|
+
.build();
|
|
984
|
+
|
|
985
|
+
const response = await fragmentWithRoute.handler(
|
|
986
|
+
new Request("http://localhost/api/test-service"),
|
|
987
|
+
);
|
|
988
|
+
|
|
989
|
+
expect(response.status).toBe(200);
|
|
990
|
+
const data = await response.json();
|
|
991
|
+
expect(data).toEqual({ result: "key-5-6" });
|
|
992
|
+
});
|
|
993
|
+
|
|
994
|
+
it("should be able to call services with bound context using inContext", async () => {
|
|
995
|
+
const definition = defineFragment("test-fragment")
|
|
996
|
+
.withDependencies(() => ({ apiKey: "key" }))
|
|
997
|
+
.withThisContext(() => {
|
|
998
|
+
const ctx = {
|
|
999
|
+
myThisNumber: 0,
|
|
1000
|
+
myThisString: "hello",
|
|
1001
|
+
};
|
|
1002
|
+
return { serviceContext: ctx, handlerContext: ctx };
|
|
1003
|
+
})
|
|
1004
|
+
.providesBaseService(({ deps, defineService }) =>
|
|
1005
|
+
defineService({
|
|
1006
|
+
method1: function () {
|
|
1007
|
+
expectTypeOf(this).toMatchObjectType<{
|
|
1008
|
+
myThisNumber: number;
|
|
1009
|
+
myThisString: string;
|
|
1010
|
+
}>();
|
|
1011
|
+
|
|
1012
|
+
console.log("this", this);
|
|
1013
|
+
|
|
1014
|
+
this.myThisNumber++;
|
|
1015
|
+
return `${deps.apiKey}-${++this.myThisNumber}`;
|
|
1016
|
+
},
|
|
1017
|
+
}),
|
|
1018
|
+
)
|
|
1019
|
+
.build();
|
|
1020
|
+
|
|
1021
|
+
const fragment = instantiate(definition).withOptions({}).build();
|
|
1022
|
+
|
|
1023
|
+
expect(fragment.services.method1).toBeDefined();
|
|
1024
|
+
const result2 = fragment.inContext(() => fragment.services.method1());
|
|
1025
|
+
expect(result2).toBe("key-2");
|
|
1026
|
+
});
|
|
1027
|
+
});
|
|
1028
|
+
|
|
1029
|
+
describe("defineRoutes with services", () => {
|
|
1030
|
+
it("should provide base services in route factory context", () => {
|
|
1031
|
+
const definition = defineFragment("test-fragment")
|
|
1032
|
+
.providesBaseService(() => ({
|
|
1033
|
+
greet: (name: string) => `Hello, ${name}!`,
|
|
1034
|
+
}))
|
|
1035
|
+
.build();
|
|
1036
|
+
|
|
1037
|
+
const routes = defineRoutes(definition).create(({ services, defineRoute }) => {
|
|
1038
|
+
// Verify base service is accessible
|
|
1039
|
+
expectTypeOf(services).toMatchObjectType<{
|
|
1040
|
+
greet: (name: string) => string;
|
|
1041
|
+
}>();
|
|
1042
|
+
|
|
1043
|
+
return [
|
|
1044
|
+
defineRoute({
|
|
1045
|
+
method: "GET",
|
|
1046
|
+
path: "/greet",
|
|
1047
|
+
handler: async (_input, { json }) => {
|
|
1048
|
+
const message = services.greet("World");
|
|
1049
|
+
return json({ message });
|
|
1050
|
+
},
|
|
1051
|
+
}),
|
|
1052
|
+
];
|
|
1053
|
+
});
|
|
1054
|
+
|
|
1055
|
+
expect(routes).toBeDefined();
|
|
1056
|
+
expect(typeof routes).toBe("function");
|
|
1057
|
+
});
|
|
1058
|
+
|
|
1059
|
+
it("should provide named services in route factory context", () => {
|
|
1060
|
+
const definition = defineFragment("test-fragment")
|
|
1061
|
+
.providesService("mathService", () => ({
|
|
1062
|
+
add: (a: number, b: number) => a + b,
|
|
1063
|
+
multiply: (a: number, b: number) => a * b,
|
|
1064
|
+
}))
|
|
1065
|
+
.build();
|
|
1066
|
+
|
|
1067
|
+
const routes = defineRoutes(definition).create(({ services, defineRoute }) => {
|
|
1068
|
+
// Verify named service is accessible
|
|
1069
|
+
expectTypeOf(services).toMatchObjectType<{
|
|
1070
|
+
mathService: {
|
|
1071
|
+
add: (a: number, b: number) => number;
|
|
1072
|
+
multiply: (a: number, b: number) => number;
|
|
1073
|
+
};
|
|
1074
|
+
}>();
|
|
1075
|
+
|
|
1076
|
+
return [
|
|
1077
|
+
defineRoute({
|
|
1078
|
+
method: "GET",
|
|
1079
|
+
path: "/math",
|
|
1080
|
+
handler: async (_input, { json }) => {
|
|
1081
|
+
const sum = services.mathService.add(2, 3);
|
|
1082
|
+
const product = services.mathService.multiply(4, 5);
|
|
1083
|
+
return json({ sum, product });
|
|
1084
|
+
},
|
|
1085
|
+
}),
|
|
1086
|
+
];
|
|
1087
|
+
});
|
|
1088
|
+
|
|
1089
|
+
expect(routes).toBeDefined();
|
|
1090
|
+
});
|
|
1091
|
+
|
|
1092
|
+
it("should provide both base and named services in route factory context", () => {
|
|
1093
|
+
const definition = defineFragment("test-fragment")
|
|
1094
|
+
.providesBaseService(() => ({
|
|
1095
|
+
baseMethod: () => "base",
|
|
1096
|
+
}))
|
|
1097
|
+
.providesService("namedService", () => ({
|
|
1098
|
+
namedMethod: () => "named",
|
|
1099
|
+
}))
|
|
1100
|
+
.build();
|
|
1101
|
+
|
|
1102
|
+
const routes = defineRoutes(definition).create(({ services, defineRoute }) => {
|
|
1103
|
+
// Verify both base and named services are accessible
|
|
1104
|
+
expectTypeOf(services.baseMethod).toBeFunction();
|
|
1105
|
+
expectTypeOf(services.namedService).toBeObject();
|
|
1106
|
+
expectTypeOf(services.namedService.namedMethod).toBeFunction();
|
|
1107
|
+
|
|
1108
|
+
return [
|
|
1109
|
+
defineRoute({
|
|
1110
|
+
method: "GET",
|
|
1111
|
+
path: "/combined",
|
|
1112
|
+
handler: async (_input, { json }) => {
|
|
1113
|
+
const base = services.baseMethod();
|
|
1114
|
+
const named = services.namedService.namedMethod();
|
|
1115
|
+
return json({ base, named });
|
|
1116
|
+
},
|
|
1117
|
+
}),
|
|
1118
|
+
];
|
|
1119
|
+
});
|
|
1120
|
+
|
|
1121
|
+
expect(routes).toBeDefined();
|
|
1122
|
+
});
|
|
1123
|
+
|
|
1124
|
+
it("should have services in route context match fragment.services at runtime", async () => {
|
|
1125
|
+
const definition = defineFragment("test-fragment")
|
|
1126
|
+
.providesBaseService(() => ({
|
|
1127
|
+
baseMethod: () => "base-value",
|
|
1128
|
+
}))
|
|
1129
|
+
.providesService("namedService", () => ({
|
|
1130
|
+
namedMethod: () => "named-value",
|
|
1131
|
+
}))
|
|
1132
|
+
.build();
|
|
1133
|
+
|
|
1134
|
+
const routes = defineRoutes(definition).create(({ services, defineRoute }) => [
|
|
1135
|
+
defineRoute({
|
|
1136
|
+
method: "GET",
|
|
1137
|
+
path: "/test",
|
|
1138
|
+
handler: async (_input, { json }) => {
|
|
1139
|
+
return json({
|
|
1140
|
+
base: services.baseMethod(),
|
|
1141
|
+
named: services.namedService.namedMethod(),
|
|
1142
|
+
});
|
|
1143
|
+
},
|
|
1144
|
+
}),
|
|
1145
|
+
]);
|
|
1146
|
+
|
|
1147
|
+
const fragment = instantiate(definition)
|
|
1148
|
+
.withRoutes([routes])
|
|
1149
|
+
.withOptions({ mountRoute: "/api" })
|
|
1150
|
+
.build();
|
|
1151
|
+
|
|
1152
|
+
// Verify fragment.services has the same structure
|
|
1153
|
+
expect(fragment.services.baseMethod()).toBe("base-value");
|
|
1154
|
+
expect(fragment.services.namedService.namedMethod()).toBe("named-value");
|
|
1155
|
+
|
|
1156
|
+
// Verify route can access services with the same structure
|
|
1157
|
+
const response = await fragment.handler(new Request("http://localhost/api/test"));
|
|
1158
|
+
expect(response.status).toBe(200);
|
|
1159
|
+
const data = await response.json();
|
|
1160
|
+
expect(data).toEqual({
|
|
1161
|
+
base: "base-value",
|
|
1162
|
+
named: "named-value",
|
|
1163
|
+
});
|
|
1164
|
+
});
|
|
1165
|
+
|
|
1166
|
+
it("should allow multiple named services in route factory context", () => {
|
|
1167
|
+
const definition = defineFragment("test-fragment")
|
|
1168
|
+
.providesService("service1", () => ({
|
|
1169
|
+
method1: () => "value1",
|
|
1170
|
+
}))
|
|
1171
|
+
.providesService("service2", () => ({
|
|
1172
|
+
method2: () => "value2",
|
|
1173
|
+
}))
|
|
1174
|
+
.providesService("service3", () => ({
|
|
1175
|
+
method3: () => "value3",
|
|
1176
|
+
}))
|
|
1177
|
+
.build();
|
|
1178
|
+
|
|
1179
|
+
const routes = defineRoutes(definition).create(({ services, defineRoute }) => {
|
|
1180
|
+
// Verify all named services are accessible
|
|
1181
|
+
expectTypeOf(services.service1).toBeObject();
|
|
1182
|
+
expectTypeOf(services.service1.method1).toBeFunction();
|
|
1183
|
+
expectTypeOf(services.service2).toBeObject();
|
|
1184
|
+
expectTypeOf(services.service2.method2).toBeFunction();
|
|
1185
|
+
expectTypeOf(services.service3).toBeObject();
|
|
1186
|
+
expectTypeOf(services.service3.method3).toBeFunction();
|
|
1187
|
+
|
|
1188
|
+
return [
|
|
1189
|
+
defineRoute({
|
|
1190
|
+
method: "GET",
|
|
1191
|
+
path: "/multi",
|
|
1192
|
+
handler: async (_input, { json }) => {
|
|
1193
|
+
return json({
|
|
1194
|
+
v1: services.service1.method1(),
|
|
1195
|
+
v2: services.service2.method2(),
|
|
1196
|
+
v3: services.service3.method3(),
|
|
1197
|
+
});
|
|
1198
|
+
},
|
|
1199
|
+
}),
|
|
1200
|
+
];
|
|
1201
|
+
});
|
|
1202
|
+
|
|
1203
|
+
expect(routes).toBeDefined();
|
|
1204
|
+
});
|
|
1205
|
+
|
|
1206
|
+
it("should provide services with dependencies in route factory context", () => {
|
|
1207
|
+
interface Config {
|
|
1208
|
+
prefix: string;
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
const definition = defineFragment<Config>("test-fragment")
|
|
1212
|
+
.withDependencies(({ config }) => ({
|
|
1213
|
+
prefix: config.prefix,
|
|
1214
|
+
}))
|
|
1215
|
+
.providesBaseService(({ deps }) => ({
|
|
1216
|
+
greet: (name: string) => `${deps.prefix} ${name}`,
|
|
1217
|
+
}))
|
|
1218
|
+
.providesService("formatter", ({ deps }) => ({
|
|
1219
|
+
format: (text: string) => `[${deps.prefix}] ${text}`,
|
|
1220
|
+
}))
|
|
1221
|
+
.build();
|
|
1222
|
+
|
|
1223
|
+
const routes = defineRoutes(definition).create(({ services, defineRoute }) => {
|
|
1224
|
+
// Verify services are accessible
|
|
1225
|
+
expectTypeOf(services).toMatchObjectType<{
|
|
1226
|
+
greet: (name: string) => string;
|
|
1227
|
+
formatter: {
|
|
1228
|
+
format: (text: string) => string;
|
|
1229
|
+
};
|
|
1230
|
+
}>();
|
|
1231
|
+
|
|
1232
|
+
return [
|
|
1233
|
+
defineRoute({
|
|
1234
|
+
method: "GET",
|
|
1235
|
+
path: "/format",
|
|
1236
|
+
handler: async (_input, { json }) => {
|
|
1237
|
+
return json({
|
|
1238
|
+
greeting: services.greet("World"),
|
|
1239
|
+
formatted: services.formatter.format("Hello"),
|
|
1240
|
+
});
|
|
1241
|
+
},
|
|
1242
|
+
}),
|
|
1243
|
+
];
|
|
1244
|
+
});
|
|
1245
|
+
|
|
1246
|
+
expect(routes).toBeDefined();
|
|
1247
|
+
});
|
|
1248
|
+
});
|
|
1249
|
+
|
|
1250
|
+
describe("linked fragments", () => {
|
|
1251
|
+
it("should instantiate linked fragments with the parent fragment", () => {
|
|
1252
|
+
interface Config {
|
|
1253
|
+
apiKey: string;
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
// Create a linked fragment definition
|
|
1257
|
+
const linkedFragmentDef = defineFragment<Config>("linked-fragment")
|
|
1258
|
+
.providesService("linkedService", () => ({
|
|
1259
|
+
getValue: () => "from-linked",
|
|
1260
|
+
}))
|
|
1261
|
+
.build();
|
|
1262
|
+
|
|
1263
|
+
// Create main fragment with linked fragment
|
|
1264
|
+
const definition = defineFragment<Config>("main-fragment")
|
|
1265
|
+
.withLinkedFragment("internal", ({ config, options }) => {
|
|
1266
|
+
return instantiate(linkedFragmentDef).withConfig(config).withOptions(options).build();
|
|
1267
|
+
})
|
|
1268
|
+
.build();
|
|
1269
|
+
|
|
1270
|
+
const fragment = instantiate(definition)
|
|
1271
|
+
.withConfig({ apiKey: "test-key" })
|
|
1272
|
+
.withOptions({ mountRoute: "/api" })
|
|
1273
|
+
.build();
|
|
1274
|
+
|
|
1275
|
+
// Verify linked fragment exists
|
|
1276
|
+
expect(Object.keys(fragment.$internal.linkedFragments).length).toBe(1);
|
|
1277
|
+
expect("internal" in fragment.$internal.linkedFragments).toBe(true);
|
|
1278
|
+
|
|
1279
|
+
const linkedFragment = fragment.$internal.linkedFragments.internal;
|
|
1280
|
+
expect(linkedFragment).toBeDefined();
|
|
1281
|
+
expect(linkedFragment?.name).toBe("linked-fragment");
|
|
1282
|
+
});
|
|
1283
|
+
|
|
1284
|
+
it("should pass config and options to linked fragments", () => {
|
|
1285
|
+
interface Config {
|
|
1286
|
+
value: string;
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
interface Options extends FragnoPublicConfig {
|
|
1290
|
+
customOption: string;
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
const linkedFragmentDef = defineFragment<Config, Options>("linked-fragment")
|
|
1294
|
+
.withDependencies(({ config, options }) => ({
|
|
1295
|
+
combined: `${config.value}-${options.customOption}`,
|
|
1296
|
+
}))
|
|
1297
|
+
.build();
|
|
1298
|
+
|
|
1299
|
+
const definition = defineFragment<Config, Options>("main-fragment")
|
|
1300
|
+
.withLinkedFragment("internal", ({ config, options }) => {
|
|
1301
|
+
return instantiate(linkedFragmentDef).withConfig(config).withOptions(options).build();
|
|
1302
|
+
})
|
|
1303
|
+
.build();
|
|
1304
|
+
|
|
1305
|
+
const fragment = instantiate(definition)
|
|
1306
|
+
.withConfig({ value: "config" })
|
|
1307
|
+
.withOptions({ customOption: "option", mountRoute: "/api" } as Options)
|
|
1308
|
+
.build();
|
|
1309
|
+
|
|
1310
|
+
const linkedFragment = fragment.$internal.linkedFragments.internal;
|
|
1311
|
+
expect(linkedFragment?.$internal.deps).toEqual({
|
|
1312
|
+
combined: "config-option",
|
|
1313
|
+
});
|
|
1314
|
+
});
|
|
1315
|
+
|
|
1316
|
+
it("should allow linked fragments to provide services", () => {
|
|
1317
|
+
const linkedFragmentDef = defineFragment("linked-fragment")
|
|
1318
|
+
.providesService("settingsService", () => ({
|
|
1319
|
+
get: (key: string) => `value-for-${key}`,
|
|
1320
|
+
set: (key: string, value: string) => {
|
|
1321
|
+
console.log(`Setting ${key} = ${value}`);
|
|
1322
|
+
},
|
|
1323
|
+
}))
|
|
1324
|
+
.build();
|
|
1325
|
+
|
|
1326
|
+
const definition = defineFragment("main-fragment")
|
|
1327
|
+
.withLinkedFragment("internal", ({ config, options }) => {
|
|
1328
|
+
return instantiate(linkedFragmentDef).withConfig(config).withOptions(options).build();
|
|
1329
|
+
})
|
|
1330
|
+
.build();
|
|
1331
|
+
|
|
1332
|
+
const fragment = instantiate(definition).withOptions({}).build();
|
|
1333
|
+
|
|
1334
|
+
const linkedFragment = fragment.$internal.linkedFragments.internal;
|
|
1335
|
+
expect(linkedFragment?.services.settingsService).toBeDefined();
|
|
1336
|
+
expect(linkedFragment?.services.settingsService.get("test")).toBe("value-for-test");
|
|
1337
|
+
});
|
|
1338
|
+
|
|
1339
|
+
it("should support multiple linked fragments", () => {
|
|
1340
|
+
const linkedFragmentDef1 = defineFragment("linked-fragment-1")
|
|
1341
|
+
.providesService("service1", () => ({ method: () => "service1" }))
|
|
1342
|
+
.build();
|
|
1343
|
+
|
|
1344
|
+
const linkedFragmentDef2 = defineFragment("linked-fragment-2")
|
|
1345
|
+
.providesService("service2", () => ({ method: () => "service2" }))
|
|
1346
|
+
.build();
|
|
1347
|
+
|
|
1348
|
+
const definition = defineFragment("main-fragment")
|
|
1349
|
+
.withLinkedFragment("internal1", ({ config, options }) => {
|
|
1350
|
+
return instantiate(linkedFragmentDef1).withConfig(config).withOptions(options).build();
|
|
1351
|
+
})
|
|
1352
|
+
.withLinkedFragment("internal2", ({ config, options }) => {
|
|
1353
|
+
return instantiate(linkedFragmentDef2).withConfig(config).withOptions(options).build();
|
|
1354
|
+
})
|
|
1355
|
+
.build();
|
|
1356
|
+
|
|
1357
|
+
const fragment = instantiate(definition).withOptions({}).build();
|
|
1358
|
+
|
|
1359
|
+
expect(Object.keys(fragment.$internal.linkedFragments).length).toBe(2);
|
|
1360
|
+
expect("internal1" in fragment.$internal.linkedFragments).toBe(true);
|
|
1361
|
+
expect("internal2" in fragment.$internal.linkedFragments).toBe(true);
|
|
1362
|
+
|
|
1363
|
+
const linked1 = fragment.$internal.linkedFragments.internal1;
|
|
1364
|
+
const linked2 = fragment.$internal.linkedFragments.internal2;
|
|
1365
|
+
|
|
1366
|
+
expect(linked1?.services.service1.method()).toBe("service1");
|
|
1367
|
+
expect(linked2?.services.service2.method()).toBe("service2");
|
|
1368
|
+
});
|
|
1369
|
+
|
|
1370
|
+
it("should pass service dependencies to linked fragments", () => {
|
|
1371
|
+
interface ExternalService {
|
|
1372
|
+
getValue: () => string;
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
const externalService: ExternalService = {
|
|
1376
|
+
getValue: () => "external-value",
|
|
1377
|
+
};
|
|
1378
|
+
|
|
1379
|
+
const linkedFragmentDef = defineFragment("linked-fragment")
|
|
1380
|
+
.usesService<"externalService", ExternalService>("externalService")
|
|
1381
|
+
.providesService("linkedService", ({ serviceDeps }) => ({
|
|
1382
|
+
getFromExternal: () => serviceDeps.externalService.getValue(),
|
|
1383
|
+
}))
|
|
1384
|
+
.build();
|
|
1385
|
+
|
|
1386
|
+
const definition = defineFragment("main-fragment")
|
|
1387
|
+
.usesService<"externalService", ExternalService>("externalService")
|
|
1388
|
+
.withLinkedFragment("internal", ({ config, options, serviceDependencies }) => {
|
|
1389
|
+
return instantiate(linkedFragmentDef)
|
|
1390
|
+
.withConfig(config)
|
|
1391
|
+
.withOptions(options)
|
|
1392
|
+
.withServices(serviceDependencies!)
|
|
1393
|
+
.build();
|
|
1394
|
+
})
|
|
1395
|
+
.build();
|
|
1396
|
+
|
|
1397
|
+
const fragment = instantiate(definition)
|
|
1398
|
+
.withOptions({})
|
|
1399
|
+
.withServices({ externalService })
|
|
1400
|
+
.build();
|
|
1401
|
+
|
|
1402
|
+
const linkedFragment = fragment.$internal.linkedFragments.internal;
|
|
1403
|
+
expect(linkedFragment?.services.linkedService.getFromExternal()).toBe("external-value");
|
|
1404
|
+
});
|
|
1405
|
+
|
|
1406
|
+
it("should expose linked fragment services as private services", () => {
|
|
1407
|
+
const linkedFragmentDef = defineFragment("linked-fragment")
|
|
1408
|
+
.providesService("linkedService", () => ({
|
|
1409
|
+
getValue: () => "from-linked",
|
|
1410
|
+
}))
|
|
1411
|
+
.build();
|
|
1412
|
+
|
|
1413
|
+
const definition = defineFragment("main-fragment")
|
|
1414
|
+
.withLinkedFragment("internal", ({ config, options }) => {
|
|
1415
|
+
return instantiate(linkedFragmentDef).withConfig(config).withOptions(options).build();
|
|
1416
|
+
})
|
|
1417
|
+
.providesService("mainService", ({ privateServices }) => ({
|
|
1418
|
+
getLinkedValue: () => {
|
|
1419
|
+
return privateServices.linkedService.getValue();
|
|
1420
|
+
},
|
|
1421
|
+
}))
|
|
1422
|
+
.build();
|
|
1423
|
+
|
|
1424
|
+
const fragment = instantiate(definition).withOptions({}).build();
|
|
1425
|
+
|
|
1426
|
+
// The main service can access linked fragment services via privateServices
|
|
1427
|
+
expect(fragment.services.mainService.getLinkedValue()).toBe("from-linked");
|
|
1428
|
+
|
|
1429
|
+
// Linked fragment services are NOT directly exposed on the main fragment
|
|
1430
|
+
// @ts-expect-error - Linked fragment service should not be accessible
|
|
1431
|
+
expect(fragment.services.linkedService).toBeUndefined();
|
|
1432
|
+
});
|
|
1433
|
+
});
|
|
1434
|
+
|
|
1435
|
+
describe("error handling", () => {
|
|
1436
|
+
it("should handle errors in route handlers", async () => {
|
|
1437
|
+
const definition = defineFragment("test-fragment").build();
|
|
1438
|
+
|
|
1439
|
+
const route = defineRoute({
|
|
1440
|
+
method: "GET",
|
|
1441
|
+
path: "/error",
|
|
1442
|
+
handler: async () => {
|
|
1443
|
+
throw new Error("Test error");
|
|
1444
|
+
},
|
|
1445
|
+
});
|
|
1446
|
+
|
|
1447
|
+
const fragment = instantiate(definition)
|
|
1448
|
+
.withRoutes([route])
|
|
1449
|
+
.withOptions({ mountRoute: "/api" })
|
|
1450
|
+
.build();
|
|
1451
|
+
|
|
1452
|
+
const request = new Request("http://localhost/api/error");
|
|
1453
|
+
const response = await fragment.handler(request);
|
|
1454
|
+
|
|
1455
|
+
expect(response.status).toBe(500);
|
|
1456
|
+
const data = await response.json();
|
|
1457
|
+
expect(data.code).toBe("INTERNAL_SERVER_ERROR");
|
|
1458
|
+
});
|
|
1459
|
+
|
|
1460
|
+
it("should handle errors in middleware", async () => {
|
|
1461
|
+
const definition = defineFragment("test-fragment").build();
|
|
1462
|
+
|
|
1463
|
+
const route = defineRoute({
|
|
1464
|
+
method: "GET",
|
|
1465
|
+
path: "/test",
|
|
1466
|
+
handler: async (_input, { json }) => {
|
|
1467
|
+
return json({ message: "test" });
|
|
1468
|
+
},
|
|
1469
|
+
});
|
|
1470
|
+
|
|
1471
|
+
const fragment = instantiate(definition)
|
|
1472
|
+
.withRoutes([route])
|
|
1473
|
+
.withOptions({ mountRoute: "/api" })
|
|
1474
|
+
.build();
|
|
1475
|
+
|
|
1476
|
+
fragment.withMiddleware(async () => {
|
|
1477
|
+
throw new Error("Middleware error");
|
|
1478
|
+
});
|
|
1479
|
+
|
|
1480
|
+
const request = new Request("http://localhost/api/test");
|
|
1481
|
+
const response = await fragment.handler(request);
|
|
1482
|
+
|
|
1483
|
+
expect(response.status).toBe(500);
|
|
1484
|
+
const data = await response.json();
|
|
1485
|
+
expect(data.code).toBe("INTERNAL_SERVER_ERROR");
|
|
1486
|
+
});
|
|
1487
|
+
});
|
|
1488
|
+
});
|