@fragno-dev/core 0.1.6 → 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 +46 -54
- package/CHANGELOG.md +12 -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 +8 -9
- package/dist/client/vue.js.map +1 -1
- package/dist/{client-DJfCJiHK.js → client-DAFHcKqA.js} +15 -8
- package/dist/client-DAFHcKqA.js.map +1 -0
- 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/internal/path-type.test.ts +7 -7
- package/src/api/internal/path.ts +1 -1
- 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/client/client-types.test.ts +4 -4
- package/src/client/client.test.ts +77 -0
- package/src/client/client.ts +31 -12
- package/src/mod.ts +11 -1
- package/src/test/test.test.ts +20 -15
- package/src/test/test.ts +48 -9
- package/dist/api-CoCkNi6h.d.ts +0 -377
- package/dist/api-CoCkNi6h.d.ts.map +0 -1
- package/dist/api-DngJDcmO.js +0 -54
- package/dist/api-DngJDcmO.js.map +0 -1
- package/dist/client-DJfCJiHK.js.map +0 -1
- package/dist/fragment-builder-8-tiECi5.d.ts +0 -408
- package/dist/fragment-builder-8-tiECi5.d.ts.map +0 -1
- package/dist/fragment-builder-DOnCVBqc.js +0 -47
- package/dist/fragment-builder-DOnCVBqc.js.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-C5Uryylh.js.map +0 -1
- package/dist/route-mGLYSUvD.d.ts +0 -26
- package/dist/route-mGLYSUvD.d.ts.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
|
});
|
|
@@ -446,39 +446,39 @@ test("MaybeExtractPathParamsOrWiden type tests", () => {
|
|
|
446
446
|
test("QueryParamsHint type tests", () => {
|
|
447
447
|
// Basic usage with string union
|
|
448
448
|
expectTypeOf<QueryParamsHint<"page" | "limit">>().toEqualTypeOf<
|
|
449
|
-
Partial<Record<"page" | "limit", string>> & Record<string, string>
|
|
449
|
+
Partial<Record<"page" | "limit", string>> & Record<string, string | undefined>
|
|
450
450
|
>();
|
|
451
451
|
|
|
452
452
|
// Single parameter hint
|
|
453
453
|
expectTypeOf<QueryParamsHint<"search">>().toEqualTypeOf<
|
|
454
|
-
Partial<Record<"search", string>> & Record<string, string>
|
|
454
|
+
Partial<Record<"search", string>> & Record<string, string | undefined>
|
|
455
455
|
>();
|
|
456
456
|
|
|
457
457
|
// Empty hint (never) - should still allow any string keys
|
|
458
458
|
expectTypeOf<QueryParamsHint<never>>().toEqualTypeOf<
|
|
459
|
-
Partial<Record<never, string>> & Record<string, string>
|
|
459
|
+
Partial<Record<never, string>> & Record<string, string | undefined>
|
|
460
460
|
>();
|
|
461
461
|
|
|
462
462
|
// With custom value type
|
|
463
463
|
expectTypeOf<QueryParamsHint<"page" | "limit", number>>().toEqualTypeOf<
|
|
464
|
-
Partial<Record<"page" | "limit", number>> & Record<string, number>
|
|
464
|
+
Partial<Record<"page" | "limit", number>> & Record<string, number | undefined>
|
|
465
465
|
>();
|
|
466
466
|
|
|
467
467
|
// With boolean value type
|
|
468
468
|
expectTypeOf<QueryParamsHint<"enabled" | "debug", boolean>>().toEqualTypeOf<
|
|
469
|
-
Partial<Record<"enabled" | "debug", boolean>> & Record<string, boolean>
|
|
469
|
+
Partial<Record<"enabled" | "debug", boolean>> & Record<string, boolean | undefined>
|
|
470
470
|
>();
|
|
471
471
|
|
|
472
472
|
// With union value type
|
|
473
473
|
type StringOrNumber = string | number;
|
|
474
474
|
expectTypeOf<QueryParamsHint<"value", StringOrNumber>>().toEqualTypeOf<
|
|
475
|
-
Partial<Record<"value", StringOrNumber>> & Record<string, StringOrNumber>
|
|
475
|
+
Partial<Record<"value", StringOrNumber>> & Record<string, StringOrNumber | undefined>
|
|
476
476
|
>();
|
|
477
477
|
|
|
478
478
|
// With custom object type
|
|
479
479
|
type CustomType = { raw: string; parsed: boolean };
|
|
480
480
|
expectTypeOf<QueryParamsHint<"data", CustomType>>().toEqualTypeOf<
|
|
481
|
-
Partial<Record<"data", CustomType>> & Record<string, CustomType>
|
|
481
|
+
Partial<Record<"data", CustomType>> & Record<string, CustomType | undefined>
|
|
482
482
|
>();
|
|
483
483
|
});
|
|
484
484
|
|
package/src/api/internal/path.ts
CHANGED
|
@@ -124,7 +124,7 @@ export type HasPathParams<T extends string> = ExtractPathParamNames<T> extends n
|
|
|
124
124
|
export type QueryParamsHint<TQueryParameters extends string, ValueType = string> = Partial<
|
|
125
125
|
Record<TQueryParameters, ValueType>
|
|
126
126
|
> &
|
|
127
|
-
Record<string, ValueType>;
|
|
127
|
+
Record<string, ValueType | undefined>;
|
|
128
128
|
|
|
129
129
|
// Runtime utilities
|
|
130
130
|
|
|
@@ -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({
|