@fragno-dev/core 0.1.7 → 0.1.8
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 +45 -53
- package/CHANGELOG.md +6 -0
- package/dist/api/api.d.ts +2 -2
- package/dist/api/api.js +3 -2
- package/dist/api/fragment-builder.d.ts +2 -4
- package/dist/api/fragment-builder.js +1 -1
- package/dist/api/fragment-instantiation.d.ts +2 -4
- package/dist/api/fragment-instantiation.js +3 -5
- package/dist/api/route.d.ts +2 -3
- package/dist/api/route.js +1 -1
- package/dist/api-BFrUCIsF.d.ts +963 -0
- package/dist/api-BFrUCIsF.d.ts.map +1 -0
- package/dist/client/client.d.ts +1 -3
- package/dist/client/client.js +4 -5
- package/dist/client/client.svelte.d.ts +2 -3
- package/dist/client/client.svelte.d.ts.map +1 -1
- package/dist/client/client.svelte.js +4 -5
- package/dist/client/client.svelte.js.map +1 -1
- package/dist/client/react.d.ts +2 -3
- package/dist/client/react.d.ts.map +1 -1
- package/dist/client/react.js +4 -5
- package/dist/client/react.js.map +1 -1
- package/dist/client/solid.d.ts +2 -3
- package/dist/client/solid.d.ts.map +1 -1
- package/dist/client/solid.js +4 -5
- package/dist/client/solid.js.map +1 -1
- package/dist/client/vanilla.d.ts +2 -3
- package/dist/client/vanilla.d.ts.map +1 -1
- package/dist/client/vanilla.js +4 -5
- package/dist/client/vanilla.js.map +1 -1
- package/dist/client/vue.d.ts +2 -3
- package/dist/client/vue.d.ts.map +1 -1
- package/dist/client/vue.js +4 -5
- package/dist/client/vue.js.map +1 -1
- package/dist/{client-C5LsYHEI.js → client-DAFHcKqA.js} +4 -4
- package/dist/{client-C5LsYHEI.js.map → client-DAFHcKqA.js.map} +1 -1
- package/dist/fragment-builder-Boh2vNHq.js +108 -0
- package/dist/fragment-builder-Boh2vNHq.js.map +1 -0
- package/dist/fragment-instantiation-DUT-HLl1.js +898 -0
- package/dist/fragment-instantiation-DUT-HLl1.js.map +1 -0
- package/dist/integrations/react-ssr.js +1 -1
- package/dist/mod.d.ts +2 -4
- package/dist/mod.js +4 -6
- package/dist/{route-C5Uryylh.js → route-C4CyNHkC.js} +8 -3
- package/dist/route-C4CyNHkC.js.map +1 -0
- package/dist/{ssr-BByDVfFD.js → ssr-kyKI7pqH.js} +1 -1
- package/dist/{ssr-BByDVfFD.js.map → ssr-kyKI7pqH.js.map} +1 -1
- package/dist/test/test.d.ts +6 -7
- package/dist/test/test.d.ts.map +1 -1
- package/dist/test/test.js +9 -7
- package/dist/test/test.js.map +1 -1
- package/package.json +1 -1
- package/src/api/api.ts +45 -6
- package/src/api/fragment-builder.ts +463 -25
- package/src/api/fragment-instantiation.test.ts +249 -7
- package/src/api/fragment-instantiation.ts +283 -16
- package/src/api/fragment-services.test.ts +462 -0
- package/src/api/fragment.test.ts +65 -17
- package/src/api/request-middleware.test.ts +6 -3
- package/src/api/route.test.ts +111 -1
- package/src/api/route.ts +323 -14
- package/src/mod.ts +11 -1
- package/src/test/test.test.ts +20 -15
- package/src/test/test.ts +48 -9
- package/dist/api-BWN97TOr.d.ts +0 -377
- package/dist/api-BWN97TOr.d.ts.map +0 -1
- package/dist/api-DngJDcmO.js +0 -54
- package/dist/api-DngJDcmO.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.map +0 -1
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
import { describe, test, expect, expectTypeOf } from "vitest";
|
|
2
|
+
import { defineFragment } from "./fragment-builder";
|
|
3
|
+
import { createFragment, instantiateFragment } from "./fragment-instantiation";
|
|
4
|
+
|
|
5
|
+
// Test service interface definitions
|
|
6
|
+
interface IEmailService {
|
|
7
|
+
sendEmail(to: string, subject: string, body: string): Promise<void>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface ILogger {
|
|
11
|
+
log(message: string): void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe("Fragment Service System", () => {
|
|
15
|
+
describe("usesService", () => {
|
|
16
|
+
test("should declare required service by default", () => {
|
|
17
|
+
const fragment = defineFragment<{}>("test-fragment").usesService<"email", IEmailService>(
|
|
18
|
+
"email",
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
expect(fragment.definition.usedServices).toBeDefined();
|
|
22
|
+
expect(fragment.definition.usedServices?.email).toEqual({ name: "email", required: true });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("should declare optional service with { optional: true }", () => {
|
|
26
|
+
const fragment = defineFragment<{}>("test-fragment").usesService<"email", IEmailService>(
|
|
27
|
+
"email",
|
|
28
|
+
{ optional: true },
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
expect(fragment.definition.usedServices).toBeDefined();
|
|
32
|
+
expect(fragment.definition.usedServices?.email).toEqual({ name: "email", required: false });
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("should support multiple required services", () => {
|
|
36
|
+
const fragment = defineFragment<{}>("test-fragment")
|
|
37
|
+
.usesService<"email", IEmailService>("email")
|
|
38
|
+
.usesService<"logger", ILogger>("logger");
|
|
39
|
+
|
|
40
|
+
expect(fragment.definition.usedServices?.email).toEqual({ name: "email", required: true });
|
|
41
|
+
expect(fragment.definition.usedServices?.logger).toEqual({ name: "logger", required: true });
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("should support mixing required and optional services", () => {
|
|
45
|
+
const fragment = defineFragment<{}>("test-fragment")
|
|
46
|
+
.usesService<"email", IEmailService>("email")
|
|
47
|
+
.usesService<"logger", ILogger>("logger", { optional: true });
|
|
48
|
+
|
|
49
|
+
expect(fragment.definition.usedServices?.email).toEqual({ name: "email", required: true });
|
|
50
|
+
expect(fragment.definition.usedServices?.logger).toEqual({ name: "logger", required: false });
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("should preserve other fragment properties", () => {
|
|
54
|
+
const fragment = defineFragment<{ apiKey: string }>("test-fragment")
|
|
55
|
+
.withDependencies(() => ({ dep: "value" }))
|
|
56
|
+
.usesService<"email", IEmailService>("email");
|
|
57
|
+
|
|
58
|
+
expect(fragment.definition.name).toBe("test-fragment");
|
|
59
|
+
expect(fragment.definition.usedServices?.email).toBeDefined();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("should have correct type inference for required service", () => {
|
|
63
|
+
const fragment = defineFragment<{}>("test").usesService<"email", IEmailService>("email");
|
|
64
|
+
|
|
65
|
+
expectTypeOf(fragment).toMatchTypeOf<{
|
|
66
|
+
definition: {
|
|
67
|
+
usedServices?: {
|
|
68
|
+
email: { name: string; required: boolean };
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
}>();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("should have correct type inference for optional service", () => {
|
|
75
|
+
const fragment = defineFragment<{}>("test").usesService<"logger", ILogger>("logger", {
|
|
76
|
+
optional: true,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
expectTypeOf(fragment).toMatchTypeOf<{
|
|
80
|
+
definition: {
|
|
81
|
+
usedServices?: {
|
|
82
|
+
logger: { name: string; required: boolean };
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
}>();
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe("providesService", () => {
|
|
90
|
+
test("should declare provided service implementation", () => {
|
|
91
|
+
const emailImpl: IEmailService = {
|
|
92
|
+
sendEmail: async () => {},
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const fragment = defineFragment<{}>("test-fragment").providesService(
|
|
96
|
+
"email",
|
|
97
|
+
({ defineService }) => defineService(emailImpl),
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
expect(fragment.definition.providedServices).toBeDefined();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("should support multiple provided services", () => {
|
|
104
|
+
const emailImpl: IEmailService = {
|
|
105
|
+
sendEmail: async () => {},
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const loggerImpl: ILogger = {
|
|
109
|
+
log: () => {},
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const _fragment = defineFragment<{}>("test-fragment")
|
|
113
|
+
.providesService("email", ({ defineService }) => defineService(emailImpl))
|
|
114
|
+
.providesService("logger", ({ defineService }) => defineService(loggerImpl));
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe("Service metadata", () => {
|
|
119
|
+
test("should store service metadata in definition", () => {
|
|
120
|
+
const fragment = defineFragment<{}>("test")
|
|
121
|
+
.usesService<"email", IEmailService>("email")
|
|
122
|
+
.usesService<"logger", ILogger>("logger", { optional: true });
|
|
123
|
+
|
|
124
|
+
expect(fragment.definition.usedServices?.email?.required).toBe(true);
|
|
125
|
+
expect(fragment.definition.usedServices?.logger?.required).toBe(false);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("should store provided services in definition", () => {
|
|
129
|
+
const emailImpl: IEmailService = {
|
|
130
|
+
sendEmail: async () => {},
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const fragment = defineFragment<{}>("test").providesService("email", ({ defineService }) =>
|
|
134
|
+
defineService(emailImpl),
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
expect(typeof fragment.definition.providedServices).toBe("object");
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("should allow fragments without any services", () => {
|
|
141
|
+
const fragment = defineFragment<{}>("test");
|
|
142
|
+
|
|
143
|
+
expect(fragment.definition.usedServices).toBeUndefined();
|
|
144
|
+
expect(fragment.definition.providedServices).toBeUndefined();
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe("Type safety", () => {
|
|
149
|
+
test("Unnamed services should have correct types (using defineService)", () => {
|
|
150
|
+
const fragment = defineFragment<{}>("test").providesService(({ defineService }) =>
|
|
151
|
+
defineService({
|
|
152
|
+
sendEmail: async () => {},
|
|
153
|
+
}),
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
const instance = createFragment(fragment, {}, [], {});
|
|
157
|
+
expect(instance.services.sendEmail).toBeDefined();
|
|
158
|
+
expectTypeOf<typeof instance.services.sendEmail>().toExtend<() => Promise<void>>();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("Named services should have correct types (using defineService)", () => {
|
|
162
|
+
const fragment = defineFragment<{}>("test").providesService("email", ({ defineService }) =>
|
|
163
|
+
defineService({
|
|
164
|
+
sendEmail: async () => {},
|
|
165
|
+
}),
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
const instance = createFragment(fragment, {}, [], {});
|
|
169
|
+
expect(instance.services.email.sendEmail).toBeDefined();
|
|
170
|
+
expectTypeOf<typeof instance.services.email.sendEmail>().toExtend<() => Promise<void>>();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("Unnamed services should have correct types (using object)", () => {
|
|
174
|
+
const fragment = defineFragment<{}>("test").providesService({
|
|
175
|
+
sendEmail: async () => {},
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const instance = createFragment(fragment, {}, [], {});
|
|
179
|
+
expect(instance.services.sendEmail).toBeDefined();
|
|
180
|
+
expectTypeOf<typeof instance.services.sendEmail>().toExtend<() => Promise<void>>();
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("Unnamed services should have correct types (using callback with context)", () => {
|
|
184
|
+
const fragment = defineFragment<{}>("test").providesService(({ defineService }) =>
|
|
185
|
+
defineService({
|
|
186
|
+
sendEmail: async () => {},
|
|
187
|
+
}),
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
const instance = createFragment(fragment, {}, [], {});
|
|
191
|
+
expect(instance.services.sendEmail).toBeDefined();
|
|
192
|
+
expectTypeOf<typeof instance.services.sendEmail>().toExtend<() => Promise<void>>();
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("Unnamed services should have correct types (using 0-arity factory)", () => {
|
|
196
|
+
const fragment = defineFragment<{}>("test").providesService(() => ({
|
|
197
|
+
sendEmail: async () => {},
|
|
198
|
+
}));
|
|
199
|
+
|
|
200
|
+
const instance = createFragment(fragment, {}, [], {});
|
|
201
|
+
expect(instance.services.sendEmail).toBeDefined();
|
|
202
|
+
expectTypeOf<typeof instance.services.sendEmail>().toExtend<() => Promise<void>>();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test("Named services should have correct types (using object)", () => {
|
|
206
|
+
const fragment = defineFragment<{}>("test").providesService("email", {
|
|
207
|
+
sendEmail: async () => {},
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
const instance = createFragment(fragment, {}, [], {});
|
|
211
|
+
expect(instance.services.email.sendEmail).toBeDefined();
|
|
212
|
+
expectTypeOf<typeof instance.services.email.sendEmail>().toExtend<() => Promise<void>>();
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test("usesService (required)", () => {
|
|
216
|
+
const fragment = defineFragment<{}>("test").usesService<"email", IEmailService>("email");
|
|
217
|
+
|
|
218
|
+
const emailImpl: IEmailService = {
|
|
219
|
+
sendEmail: async () => {},
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const instance = createFragment(
|
|
223
|
+
fragment,
|
|
224
|
+
{},
|
|
225
|
+
[],
|
|
226
|
+
{},
|
|
227
|
+
{
|
|
228
|
+
email: emailImpl,
|
|
229
|
+
},
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
expectTypeOf<typeof instance.services.email.sendEmail>().toExtend<
|
|
233
|
+
(to: string, subject: string, body: string) => void
|
|
234
|
+
>();
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test("usesService (required) - builder style", () => {
|
|
238
|
+
const fragment = defineFragment<{}>("test").usesService<"email", IEmailService>("email");
|
|
239
|
+
|
|
240
|
+
const emailImpl: IEmailService = {
|
|
241
|
+
sendEmail: async () => {},
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const instance = instantiateFragment(fragment).withServices({ email: emailImpl }).build();
|
|
245
|
+
|
|
246
|
+
expectTypeOf<typeof instance.services.email.sendEmail>().toExtend<
|
|
247
|
+
(to: string, subject: string, body: string) => void
|
|
248
|
+
>();
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test("usesService (optional)", () => {
|
|
252
|
+
const fragment = defineFragment<{}>("test").usesService<"email", IEmailService>("email", {
|
|
253
|
+
optional: true,
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
const instance = createFragment(fragment, {}, [], {});
|
|
257
|
+
// For optional services, the service itself might be undefined
|
|
258
|
+
expectTypeOf<typeof instance.services.email>().toExtend<IEmailService | undefined>();
|
|
259
|
+
|
|
260
|
+
// If provided, the service should have the correct type
|
|
261
|
+
if (instance.services.email) {
|
|
262
|
+
expectTypeOf<typeof instance.services.email.sendEmail>().toExtend<
|
|
263
|
+
(to: string, subject: string, body: string) => Promise<void>
|
|
264
|
+
>();
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
test("provided services should have correct types", () => {
|
|
269
|
+
const emailImpl: IEmailService = {
|
|
270
|
+
sendEmail: async () => {},
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
const fragment = defineFragment<{}>("test").providesService("email", ({ defineService }) =>
|
|
274
|
+
defineService(emailImpl),
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
// providedServices stores an object with service names as keys and factory functions as values
|
|
278
|
+
expect(fragment.definition.providedServices).toBeDefined();
|
|
279
|
+
expect(typeof fragment.definition.providedServices).toBe("object");
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
test("Named services should have correct types (using callback with context)", () => {
|
|
283
|
+
const fragment = defineFragment<{}>("test").providesService("email", ({ defineService }) =>
|
|
284
|
+
defineService({
|
|
285
|
+
sendEmail: async () => {},
|
|
286
|
+
}),
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
const instance = createFragment(fragment, {}, [], {});
|
|
290
|
+
expect(instance.services.email.sendEmail).toBeDefined();
|
|
291
|
+
expectTypeOf<typeof instance.services.email.sendEmail>().toExtend<() => Promise<void>>();
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test("Named services should have correct types (using 0-arity factory)", () => {
|
|
295
|
+
const fragment = defineFragment<{}>("test").providesService("email", () => ({
|
|
296
|
+
sendEmail: async () => {},
|
|
297
|
+
}));
|
|
298
|
+
|
|
299
|
+
const instance = createFragment(fragment, {}, [], {});
|
|
300
|
+
expect(instance.services.email.sendEmail).toBeDefined();
|
|
301
|
+
expectTypeOf<typeof instance.services.email.sendEmail>().toExtend<() => Promise<void>>();
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
describe("Error handling", () => {
|
|
306
|
+
test("should throw error when required service is not provided", () => {
|
|
307
|
+
const fragment = defineFragment<{}>("test").usesService<"email", IEmailService>("email");
|
|
308
|
+
|
|
309
|
+
expect(() => {
|
|
310
|
+
createFragment(fragment, {}, [], {});
|
|
311
|
+
}).toThrow("Fragment 'test' requires service 'email' but it was not provided");
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
test("should not throw when optional service is not provided", () => {
|
|
315
|
+
const fragment = defineFragment<{}>("test").usesService<"email", IEmailService>("email", {
|
|
316
|
+
optional: true,
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
expect(() => {
|
|
320
|
+
createFragment(fragment, {}, [], {});
|
|
321
|
+
}).not.toThrow();
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
describe("Service dependencies and composition", () => {
|
|
326
|
+
test("provided service can access used services", () => {
|
|
327
|
+
const emailImpl: IEmailService = {
|
|
328
|
+
sendEmail: async () => {},
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
const fragment = defineFragment<{}>("test")
|
|
332
|
+
.usesService<"email", IEmailService>("email")
|
|
333
|
+
.providesService(({ deps }) => ({
|
|
334
|
+
sendWelcomeEmail: async (to: string) => {
|
|
335
|
+
await deps.email.sendEmail(to, "Welcome", "Welcome to our service!");
|
|
336
|
+
},
|
|
337
|
+
}));
|
|
338
|
+
|
|
339
|
+
const instance = createFragment(fragment, {}, [], {}, { email: emailImpl });
|
|
340
|
+
|
|
341
|
+
expect(instance.services.sendWelcomeEmail).toBeDefined();
|
|
342
|
+
expect(typeof instance.services.sendWelcomeEmail).toBe("function");
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
test("provided service can access used services - builder style", () => {
|
|
346
|
+
const emailImpl: IEmailService = {
|
|
347
|
+
sendEmail: async () => {},
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
const fragment = defineFragment<{}>("test")
|
|
351
|
+
.usesService<"email", IEmailService>("email")
|
|
352
|
+
.providesService(({ deps }) => ({
|
|
353
|
+
sendWelcomeEmail: async (to: string) => {
|
|
354
|
+
await deps.email.sendEmail(to, "Welcome", "Welcome to our service!");
|
|
355
|
+
},
|
|
356
|
+
}));
|
|
357
|
+
|
|
358
|
+
const instance = instantiateFragment(fragment).withServices({ email: emailImpl }).build();
|
|
359
|
+
|
|
360
|
+
expect(instance.services.sendWelcomeEmail).toBeDefined();
|
|
361
|
+
expect(typeof instance.services.sendWelcomeEmail).toBe("function");
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
test("provided service can access config", () => {
|
|
365
|
+
const fragment = defineFragment<{ apiKey: string }>("test").providesService(({ config }) => ({
|
|
366
|
+
getApiKey: () => config.apiKey,
|
|
367
|
+
}));
|
|
368
|
+
|
|
369
|
+
const instance = createFragment(fragment, { apiKey: "test-key" }, [], {});
|
|
370
|
+
|
|
371
|
+
expect(instance.services.getApiKey()).toBe("test-key");
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
test("provided service can access deps from withDependencies", () => {
|
|
375
|
+
const fragment = defineFragment<{ apiKey: string }>("test")
|
|
376
|
+
.withDependencies(({ config }) => ({
|
|
377
|
+
client: { key: config.apiKey },
|
|
378
|
+
}))
|
|
379
|
+
.providesService(({ deps }) => ({
|
|
380
|
+
getClient: () => deps.client,
|
|
381
|
+
}));
|
|
382
|
+
|
|
383
|
+
const instance = createFragment(fragment, { apiKey: "test-key" }, [], {});
|
|
384
|
+
|
|
385
|
+
expect(instance.services.getClient()).toEqual({ key: "test-key" });
|
|
386
|
+
});
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
describe("Service chaining and multiple services", () => {
|
|
390
|
+
test("should support chaining multiple provided services", () => {
|
|
391
|
+
const fragment = defineFragment<{}>("test")
|
|
392
|
+
.providesService("email", {
|
|
393
|
+
sendEmail: async () => {},
|
|
394
|
+
})
|
|
395
|
+
.providesService("logger", {
|
|
396
|
+
log: () => {},
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
const instance = createFragment(fragment, {}, [], {});
|
|
400
|
+
expect(instance.services.email.sendEmail).toBeDefined();
|
|
401
|
+
expect(instance.services.logger.log).toBeDefined();
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
test("should support mixing unnamed and named provided services", () => {
|
|
405
|
+
const fragment = defineFragment<{}>("test")
|
|
406
|
+
.providesService({
|
|
407
|
+
helper: () => "help",
|
|
408
|
+
})
|
|
409
|
+
.providesService("email", {
|
|
410
|
+
sendEmail: async () => {},
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
const instance = createFragment(fragment, {}, [], {});
|
|
414
|
+
expect(instance.services.helper).toBeDefined();
|
|
415
|
+
expect(instance.services.email.sendEmail).toBeDefined();
|
|
416
|
+
});
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
describe("Optional service runtime behavior", () => {
|
|
420
|
+
test("should handle optional service when not provided", () => {
|
|
421
|
+
const fragment = defineFragment<{}>("test")
|
|
422
|
+
.usesService<"email", IEmailService>("email", { optional: true })
|
|
423
|
+
.providesService(({ deps }) => ({
|
|
424
|
+
maybeSendEmail: async (to: string) => {
|
|
425
|
+
if (deps.email) {
|
|
426
|
+
await deps.email.sendEmail(to, "Subject", "Body");
|
|
427
|
+
return true;
|
|
428
|
+
}
|
|
429
|
+
return false;
|
|
430
|
+
},
|
|
431
|
+
}));
|
|
432
|
+
|
|
433
|
+
const instance = createFragment(fragment, {}, [], {});
|
|
434
|
+
|
|
435
|
+
expect(instance.services.maybeSendEmail).toBeDefined();
|
|
436
|
+
// Should not throw when optional service is not provided
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
test("should handle optional service when provided", () => {
|
|
440
|
+
const emailImpl: IEmailService = {
|
|
441
|
+
sendEmail: async () => {},
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
const fragment = defineFragment<{}>("test")
|
|
445
|
+
.usesService<"email", IEmailService>("email", { optional: true })
|
|
446
|
+
.providesService(({ deps }) => ({
|
|
447
|
+
maybeSendEmail: async (to: string) => {
|
|
448
|
+
if (deps.email) {
|
|
449
|
+
await deps.email.sendEmail(to, "Subject", "Body");
|
|
450
|
+
return true;
|
|
451
|
+
}
|
|
452
|
+
return false;
|
|
453
|
+
},
|
|
454
|
+
}));
|
|
455
|
+
|
|
456
|
+
const instance = createFragment(fragment, {}, [], {}, { email: emailImpl });
|
|
457
|
+
|
|
458
|
+
expect(instance.services.email).toBeDefined();
|
|
459
|
+
expect(instance.services.maybeSendEmail).toBeDefined();
|
|
460
|
+
});
|
|
461
|
+
});
|
|
462
|
+
});
|
package/src/api/fragment.test.ts
CHANGED
|
@@ -58,7 +58,7 @@ describe("new-fragment API", () => {
|
|
|
58
58
|
>();
|
|
59
59
|
});
|
|
60
60
|
|
|
61
|
-
test("
|
|
61
|
+
test("providesService has access to dependencies and config", () => {
|
|
62
62
|
const _config = {
|
|
63
63
|
apiKey: "test-key",
|
|
64
64
|
baseUrl: "https://api.example.com",
|
|
@@ -73,11 +73,11 @@ describe("new-fragment API", () => {
|
|
|
73
73
|
|
|
74
74
|
return { httpClient: { baseUrl: config.baseUrl } };
|
|
75
75
|
})
|
|
76
|
-
.
|
|
76
|
+
.providesService(({ config, deps, defineService }) => {
|
|
77
77
|
expectTypeOf(config).toEqualTypeOf<typeof _config>();
|
|
78
78
|
expectTypeOf(deps).toEqualTypeOf<{ httpClient: { baseUrl: string } }>();
|
|
79
79
|
|
|
80
|
-
return {
|
|
80
|
+
return defineService({
|
|
81
81
|
userService: {
|
|
82
82
|
getUser: async (id: string) => ({ id, name: "Test User" }),
|
|
83
83
|
},
|
|
@@ -85,7 +85,7 @@ describe("new-fragment API", () => {
|
|
|
85
85
|
get: (_key: string): string => crypto.randomUUID(),
|
|
86
86
|
set: (_key: string, _value: string) => {},
|
|
87
87
|
},
|
|
88
|
-
};
|
|
88
|
+
});
|
|
89
89
|
});
|
|
90
90
|
|
|
91
91
|
expectTypeOf<typeof _fragment>().toEqualTypeOf<
|
|
@@ -165,7 +165,9 @@ describe("new-fragment API", () => {
|
|
|
165
165
|
expectTypeOf(lib2).toEqualTypeOf<
|
|
166
166
|
FragmentBuilder<typeof _config, { dep1: string }, Empty, Empty>
|
|
167
167
|
>();
|
|
168
|
-
const lib3 = lib2.
|
|
168
|
+
const lib3 = lib2.providesService(({ defineService }) =>
|
|
169
|
+
defineService({ service1: "value1" }),
|
|
170
|
+
);
|
|
169
171
|
expectTypeOf(lib3).toEqualTypeOf<
|
|
170
172
|
FragmentBuilder<typeof _config, { dep1: string }, { service1: string }, Empty>
|
|
171
173
|
>();
|
|
@@ -182,9 +184,11 @@ describe("new-fragment API", () => {
|
|
|
182
184
|
.withDependencies(({ config }) => ({
|
|
183
185
|
client: `Client for ${config.apiKey}`,
|
|
184
186
|
}))
|
|
185
|
-
.
|
|
186
|
-
|
|
187
|
-
|
|
187
|
+
.providesService(({ deps, defineService }) =>
|
|
188
|
+
defineService({
|
|
189
|
+
service: `Service using ${deps.client}`,
|
|
190
|
+
}),
|
|
191
|
+
);
|
|
188
192
|
|
|
189
193
|
expect(fragment.definition.name).toBe("my-lib");
|
|
190
194
|
expect(fragment.definition.dependencies).toBeDefined();
|
|
@@ -220,9 +224,11 @@ describe("new-fragment API", () => {
|
|
|
220
224
|
.withDependencies(() => ({
|
|
221
225
|
formatter: (s: string) => s.toUpperCase(),
|
|
222
226
|
}))
|
|
223
|
-
.
|
|
224
|
-
|
|
225
|
-
|
|
227
|
+
.providesService(({ defineService }) =>
|
|
228
|
+
defineService({
|
|
229
|
+
logger: { log: (s: string) => console.log(s) },
|
|
230
|
+
}),
|
|
231
|
+
);
|
|
226
232
|
|
|
227
233
|
const fragment = createFragment(fragmentDef, { prefix: "Hello" }, [routeFactory], {});
|
|
228
234
|
|
|
@@ -297,7 +303,7 @@ describe("new-fragment API", () => {
|
|
|
297
303
|
|
|
298
304
|
const fragmentDef = defineFragment("test")
|
|
299
305
|
.withDependencies(() => ({ tool: "hammer" }))
|
|
300
|
-
.
|
|
306
|
+
.providesService(({ defineService }) => defineService({ storage: "memory" }));
|
|
301
307
|
|
|
302
308
|
createFragment(fragmentDef, { setting: "value" }, [routeFactory], {});
|
|
303
309
|
|
|
@@ -309,11 +315,13 @@ describe("new-fragment API", () => {
|
|
|
309
315
|
|
|
310
316
|
describe("Type constraints", () => {
|
|
311
317
|
test("Services must extend Record<string, unknown>", () => {
|
|
312
|
-
const fragmentDef = defineFragment("test").
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
318
|
+
const fragmentDef = defineFragment("test").providesService(({ defineService }) =>
|
|
319
|
+
defineService({
|
|
320
|
+
validService: { method: () => {} },
|
|
321
|
+
anotherService: "string value",
|
|
322
|
+
numberService: 123,
|
|
323
|
+
}),
|
|
324
|
+
);
|
|
317
325
|
|
|
318
326
|
const _fragment = createFragment(fragmentDef, {}, [], {});
|
|
319
327
|
|
|
@@ -534,4 +542,44 @@ describe("new-fragment API", () => {
|
|
|
534
542
|
expect(fragment.mountRoute).toBe("/custom");
|
|
535
543
|
});
|
|
536
544
|
});
|
|
545
|
+
|
|
546
|
+
describe("Route handler this context", () => {
|
|
547
|
+
test("this context type is RequestThisContext for standard fragments", () => {
|
|
548
|
+
const fragmentDef = defineFragment("test");
|
|
549
|
+
|
|
550
|
+
const routesFactory = defineRoutes().create(() => {
|
|
551
|
+
return [
|
|
552
|
+
defineRoute({
|
|
553
|
+
method: "GET",
|
|
554
|
+
path: "/test",
|
|
555
|
+
handler: async function (_, { json }) {
|
|
556
|
+
// this should be RequestThisContext
|
|
557
|
+
// (we can't easily test the exact type due to how TypeScript handles 'this')
|
|
558
|
+
expect(this).toBeDefined();
|
|
559
|
+
expect(typeof this).toBe("object");
|
|
560
|
+
return json({ ok: true });
|
|
561
|
+
},
|
|
562
|
+
}),
|
|
563
|
+
];
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
const _fragment = createFragment(fragmentDef, {}, [routesFactory], {});
|
|
567
|
+
expect(_fragment).toBeDefined();
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
test("defineRoute without defineRoutes defaults to RequestThisContext", () => {
|
|
571
|
+
const route = defineRoute({
|
|
572
|
+
method: "GET",
|
|
573
|
+
path: "/test",
|
|
574
|
+
handler: async function (_, { json }) {
|
|
575
|
+
// this defaults to RequestThisContext
|
|
576
|
+
expect(this).toBeDefined();
|
|
577
|
+
expect(typeof this).toBe("object");
|
|
578
|
+
return json({ ok: true });
|
|
579
|
+
},
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
expect(route).toBeDefined();
|
|
583
|
+
});
|
|
584
|
+
});
|
|
537
585
|
});
|
|
@@ -9,9 +9,12 @@ describe("Request Middleware", () => {
|
|
|
9
9
|
test("middleware can intercept and return early", async () => {
|
|
10
10
|
const config = { apiKey: "test" };
|
|
11
11
|
|
|
12
|
-
const fragment = defineFragment<typeof config>("test-lib").
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
const fragment = defineFragment<typeof config>("test-lib").providesService(
|
|
13
|
+
({ defineService }) =>
|
|
14
|
+
defineService({
|
|
15
|
+
auth: { isAuthorized: (token?: string) => token === "valid-token" },
|
|
16
|
+
}),
|
|
17
|
+
);
|
|
15
18
|
|
|
16
19
|
const routes = [
|
|
17
20
|
defineRoute({
|