@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,727 @@
|
|
|
1
|
+
import { describe, test, expect, expectTypeOf } from "vitest";
|
|
2
|
+
import { defineFragment } from "./fragment-definition-builder";
|
|
3
|
+
import { instantiate } from "./fragment-instantiator";
|
|
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 definition = defineFragment("test-fragment")
|
|
18
|
+
.usesService<"email", IEmailService>("email")
|
|
19
|
+
.build();
|
|
20
|
+
|
|
21
|
+
expect(definition.serviceDependencies).toBeDefined();
|
|
22
|
+
expect(definition.serviceDependencies?.email).toEqual({ name: "email", required: true });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("should declare optional service with usesOptionalService", () => {
|
|
26
|
+
const definition = defineFragment("test-fragment")
|
|
27
|
+
.usesOptionalService<"email", IEmailService>("email")
|
|
28
|
+
.build();
|
|
29
|
+
|
|
30
|
+
expect(definition.serviceDependencies).toBeDefined();
|
|
31
|
+
expect(definition.serviceDependencies?.email).toEqual({ name: "email", required: false });
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("should support multiple required services", () => {
|
|
35
|
+
const definition = defineFragment("test-fragment")
|
|
36
|
+
.usesService<"email", IEmailService>("email")
|
|
37
|
+
.usesService<"logger", ILogger>("logger")
|
|
38
|
+
.build();
|
|
39
|
+
|
|
40
|
+
expect(definition.serviceDependencies?.email).toEqual({ name: "email", required: true });
|
|
41
|
+
expect(definition.serviceDependencies?.logger).toEqual({ name: "logger", required: true });
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("should support mixing required and optional services", () => {
|
|
45
|
+
const definition = defineFragment("test-fragment")
|
|
46
|
+
.usesService<"email", IEmailService>("email")
|
|
47
|
+
.usesOptionalService<"logger", ILogger>("logger")
|
|
48
|
+
.build();
|
|
49
|
+
|
|
50
|
+
expect(definition.serviceDependencies?.email).toEqual({ name: "email", required: true });
|
|
51
|
+
expect(definition.serviceDependencies?.logger).toEqual({ name: "logger", required: false });
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("should preserve other fragment properties", () => {
|
|
55
|
+
const definition = defineFragment<{ apiKey: string }>("test-fragment")
|
|
56
|
+
.withDependencies(() => ({ dep: "value" }))
|
|
57
|
+
.usesService<"email", IEmailService>("email")
|
|
58
|
+
.build();
|
|
59
|
+
|
|
60
|
+
expect(definition.name).toBe("test-fragment");
|
|
61
|
+
expect(definition.serviceDependencies?.email).toBeDefined();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("should have correct type inference for required service", () => {
|
|
65
|
+
const definition = defineFragment("test")
|
|
66
|
+
.usesService<"email", IEmailService>("email")
|
|
67
|
+
.build();
|
|
68
|
+
|
|
69
|
+
expect(definition.serviceDependencies?.email).toBeDefined();
|
|
70
|
+
expect(definition.serviceDependencies?.email?.required).toBe(true);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("should have correct type inference for optional service", () => {
|
|
74
|
+
const definition = defineFragment("test")
|
|
75
|
+
.usesOptionalService<"logger", ILogger>("logger")
|
|
76
|
+
.build();
|
|
77
|
+
|
|
78
|
+
expect(definition.serviceDependencies?.logger).toBeDefined();
|
|
79
|
+
expect(definition.serviceDependencies?.logger?.required).toBe(false);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe("providesService", () => {
|
|
84
|
+
test("should declare provided service implementation", () => {
|
|
85
|
+
const emailImpl: IEmailService = {
|
|
86
|
+
sendEmail: async () => {},
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const definition = defineFragment("test-fragment")
|
|
90
|
+
.providesService("email", () => emailImpl)
|
|
91
|
+
.build();
|
|
92
|
+
|
|
93
|
+
expect(definition.namedServices).toBeDefined();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("should support multiple provided services", () => {
|
|
97
|
+
const emailImpl: IEmailService = {
|
|
98
|
+
sendEmail: async () => {},
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const loggerImpl: ILogger = {
|
|
102
|
+
log: () => {},
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const _definition = defineFragment("test-fragment")
|
|
106
|
+
.providesService("email", () => emailImpl)
|
|
107
|
+
.providesService("logger", () => loggerImpl)
|
|
108
|
+
.build();
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe("Service metadata", () => {
|
|
113
|
+
test("should store service metadata in definition", () => {
|
|
114
|
+
const definition = defineFragment("test")
|
|
115
|
+
.usesService<"email", IEmailService>("email")
|
|
116
|
+
.usesOptionalService<"logger", ILogger>("logger")
|
|
117
|
+
.build();
|
|
118
|
+
|
|
119
|
+
expect(definition.serviceDependencies?.email?.required).toBe(true);
|
|
120
|
+
expect(definition.serviceDependencies?.logger?.required).toBe(false);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("should store provided services in definition", () => {
|
|
124
|
+
const emailImpl: IEmailService = {
|
|
125
|
+
sendEmail: async () => {},
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const definition = defineFragment("test")
|
|
129
|
+
.providesService("email", () => emailImpl)
|
|
130
|
+
.build();
|
|
131
|
+
|
|
132
|
+
expect(typeof definition.namedServices).toBe("object");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("should allow fragments without any services", () => {
|
|
136
|
+
const definition = defineFragment("test").build();
|
|
137
|
+
|
|
138
|
+
expect(definition.serviceDependencies).toBeUndefined();
|
|
139
|
+
expect(definition.namedServices).toBeUndefined();
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe("Type safety", () => {
|
|
144
|
+
test("Unnamed services should have correct types (using providesBaseService)", () => {
|
|
145
|
+
const definition = defineFragment("test")
|
|
146
|
+
.providesBaseService(() => ({
|
|
147
|
+
sendEmail: async () => {},
|
|
148
|
+
}))
|
|
149
|
+
.build();
|
|
150
|
+
|
|
151
|
+
const instance = instantiate(definition).withOptions({}).build();
|
|
152
|
+
expect(instance.services.sendEmail).toBeDefined();
|
|
153
|
+
expectTypeOf<typeof instance.services.sendEmail>().toExtend<() => Promise<void>>();
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("Named services should have correct types", () => {
|
|
157
|
+
const definition = defineFragment("test")
|
|
158
|
+
.providesService("email", () => ({
|
|
159
|
+
sendEmail: async () => {},
|
|
160
|
+
}))
|
|
161
|
+
.build();
|
|
162
|
+
|
|
163
|
+
const instance = instantiate(definition).withOptions({}).build();
|
|
164
|
+
expect(instance.services.email.sendEmail).toBeDefined();
|
|
165
|
+
expectTypeOf<typeof instance.services.email.sendEmail>().toExtend<() => Promise<void>>();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("Unnamed services should have correct types (using factory)", () => {
|
|
169
|
+
const definition = defineFragment("test")
|
|
170
|
+
.providesBaseService(() => ({
|
|
171
|
+
sendEmail: async () => {},
|
|
172
|
+
}))
|
|
173
|
+
.build();
|
|
174
|
+
|
|
175
|
+
const instance = instantiate(definition).withOptions({}).build();
|
|
176
|
+
expect(instance.services.sendEmail).toBeDefined();
|
|
177
|
+
expectTypeOf<typeof instance.services.sendEmail>().toExtend<() => Promise<void>>();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test("Unnamed services should have correct types (using callback with context)", () => {
|
|
181
|
+
const definition = defineFragment("test")
|
|
182
|
+
.providesBaseService(() => ({
|
|
183
|
+
sendEmail: async () => {},
|
|
184
|
+
}))
|
|
185
|
+
.build();
|
|
186
|
+
|
|
187
|
+
const instance = instantiate(definition).withOptions({}).build();
|
|
188
|
+
expect(instance.services.sendEmail).toBeDefined();
|
|
189
|
+
expectTypeOf<typeof instance.services.sendEmail>().toExtend<() => Promise<void>>();
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test("Unnamed services should have correct types (using 0-arity factory)", () => {
|
|
193
|
+
const definition = defineFragment("test")
|
|
194
|
+
.providesBaseService(() => ({
|
|
195
|
+
sendEmail: async () => {},
|
|
196
|
+
}))
|
|
197
|
+
.build();
|
|
198
|
+
|
|
199
|
+
const instance = instantiate(definition).withOptions({}).build();
|
|
200
|
+
expect(instance.services.sendEmail).toBeDefined();
|
|
201
|
+
expectTypeOf<typeof instance.services.sendEmail>().toExtend<() => Promise<void>>();
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("Named services should have correct types (using factory)", () => {
|
|
205
|
+
const definition = defineFragment("test")
|
|
206
|
+
.providesService("email", () => ({
|
|
207
|
+
sendEmail: async () => {},
|
|
208
|
+
}))
|
|
209
|
+
.build();
|
|
210
|
+
|
|
211
|
+
const instance = instantiate(definition).withOptions({}).build();
|
|
212
|
+
expect(instance.services.email.sendEmail).toBeDefined();
|
|
213
|
+
expectTypeOf<typeof instance.services.email.sendEmail>().toExtend<() => Promise<void>>();
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test("usesService (required)", () => {
|
|
217
|
+
const definition = defineFragment("test")
|
|
218
|
+
.usesService<"email", IEmailService>("email")
|
|
219
|
+
.providesBaseService(({ serviceDeps }) => ({
|
|
220
|
+
sendEmail: (to: string, subject: string, body: string) => {
|
|
221
|
+
return serviceDeps.email.sendEmail(to, subject, body);
|
|
222
|
+
},
|
|
223
|
+
}))
|
|
224
|
+
.build();
|
|
225
|
+
|
|
226
|
+
const emailImpl: IEmailService = {
|
|
227
|
+
sendEmail: async () => {},
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const instance = instantiate(definition)
|
|
231
|
+
.withOptions({})
|
|
232
|
+
.withServices({ email: emailImpl })
|
|
233
|
+
.build();
|
|
234
|
+
|
|
235
|
+
expect(instance.services.sendEmail).toBeDefined();
|
|
236
|
+
expectTypeOf<typeof instance.services.sendEmail>().toExtend<
|
|
237
|
+
(to: string, subject: string, body: string) => void
|
|
238
|
+
>();
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test("usesService (required) - builder style", () => {
|
|
242
|
+
const definition = defineFragment("test")
|
|
243
|
+
.usesService<"email", IEmailService>("email")
|
|
244
|
+
.providesBaseService(({ serviceDeps }) => ({
|
|
245
|
+
sendEmail: (to: string, subject: string, body: string) => {
|
|
246
|
+
return serviceDeps.email.sendEmail(to, subject, body);
|
|
247
|
+
},
|
|
248
|
+
}))
|
|
249
|
+
.build();
|
|
250
|
+
|
|
251
|
+
const emailImpl: IEmailService = {
|
|
252
|
+
sendEmail: async () => {},
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
const instance = instantiate(definition)
|
|
256
|
+
.withServices({ email: emailImpl })
|
|
257
|
+
.withOptions({})
|
|
258
|
+
.build();
|
|
259
|
+
|
|
260
|
+
expect(instance.services.sendEmail).toBeDefined();
|
|
261
|
+
expectTypeOf<typeof instance.services.sendEmail>().toExtend<
|
|
262
|
+
(to: string, subject: string, body: string) => void
|
|
263
|
+
>();
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test("usesOptionalService", () => {
|
|
267
|
+
const definition = defineFragment("test")
|
|
268
|
+
.usesOptionalService<"email", IEmailService>("email")
|
|
269
|
+
.providesBaseService(({ serviceDeps }) => ({
|
|
270
|
+
sendEmail: serviceDeps.email
|
|
271
|
+
? (to: string, subject: string, body: string) =>
|
|
272
|
+
serviceDeps.email!.sendEmail(to, subject, body)
|
|
273
|
+
: undefined,
|
|
274
|
+
}))
|
|
275
|
+
.build();
|
|
276
|
+
|
|
277
|
+
const instance = instantiate(definition).withOptions({}).build();
|
|
278
|
+
|
|
279
|
+
// For optional services, the wrapped service method might be undefined
|
|
280
|
+
expect(instance.services.sendEmail).toBeUndefined();
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
test("provided services should have correct types", () => {
|
|
284
|
+
const emailImpl: IEmailService = {
|
|
285
|
+
sendEmail: async () => {},
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
const definition = defineFragment("test")
|
|
289
|
+
.providesService("email", () => emailImpl)
|
|
290
|
+
.build();
|
|
291
|
+
|
|
292
|
+
// namedServices stores an object with service names as keys and factory functions as values
|
|
293
|
+
expect(definition.namedServices).toBeDefined();
|
|
294
|
+
expect(typeof definition.namedServices).toBe("object");
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test("Named services should have correct types (using callback with context)", () => {
|
|
298
|
+
const definition = defineFragment("test")
|
|
299
|
+
.providesService("email", () => ({
|
|
300
|
+
sendEmail: async () => {},
|
|
301
|
+
}))
|
|
302
|
+
.build();
|
|
303
|
+
|
|
304
|
+
const instance = instantiate(definition).withOptions({}).build();
|
|
305
|
+
expect(instance.services.email.sendEmail).toBeDefined();
|
|
306
|
+
expectTypeOf<typeof instance.services.email.sendEmail>().toExtend<() => Promise<void>>();
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test("Named services should have correct types (using 0-arity factory)", () => {
|
|
310
|
+
const definition = defineFragment("test")
|
|
311
|
+
.providesService("email", () => ({
|
|
312
|
+
sendEmail: async () => {},
|
|
313
|
+
}))
|
|
314
|
+
.build();
|
|
315
|
+
|
|
316
|
+
const instance = instantiate(definition).withOptions({}).build();
|
|
317
|
+
expect(instance.services.email.sendEmail).toBeDefined();
|
|
318
|
+
expectTypeOf<typeof instance.services.email.sendEmail>().toExtend<() => Promise<void>>();
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
test("Type mismatch when using a service", () => {
|
|
322
|
+
interface ExpectedService {
|
|
323
|
+
throwDice: () => 1 | 2 | 3 | 4 | 5 | 6;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const definition = defineFragment("test")
|
|
327
|
+
.usesService<"expected", ExpectedService>("expected")
|
|
328
|
+
.providesBaseService(({ serviceDeps }) => ({
|
|
329
|
+
throwDice: () => serviceDeps.expected.throwDice(),
|
|
330
|
+
}))
|
|
331
|
+
.build();
|
|
332
|
+
|
|
333
|
+
interface ActualService {
|
|
334
|
+
throwDice: () => number;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const actualService: ActualService = {
|
|
338
|
+
throwDice: () => 1,
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
const instance = instantiate(definition)
|
|
342
|
+
// @ts-expect-error - Type mismatch
|
|
343
|
+
.withServices({ expected: actualService })
|
|
344
|
+
.withOptions({})
|
|
345
|
+
.build();
|
|
346
|
+
|
|
347
|
+
// The wrapped service on the instance has the correct type based on the declared service
|
|
348
|
+
expect(instance.services.throwDice).toBeDefined();
|
|
349
|
+
expectTypeOf<typeof instance.services.throwDice>().toExtend<() => 1 | 2 | 3 | 4 | 5 | 6>();
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
describe("Error handling", () => {
|
|
354
|
+
test("should throw error when required service is not provided", () => {
|
|
355
|
+
const definition = defineFragment("test")
|
|
356
|
+
.usesService<"email", IEmailService>("email")
|
|
357
|
+
.build();
|
|
358
|
+
|
|
359
|
+
expect(() => {
|
|
360
|
+
instantiate(definition).withOptions({}).build();
|
|
361
|
+
}).toThrow("Fragment 'test' requires service 'email' but it was not provided");
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
test("should not throw when optional service is not provided", () => {
|
|
365
|
+
const definition = defineFragment("test")
|
|
366
|
+
.usesOptionalService<"email", IEmailService>("email")
|
|
367
|
+
.build();
|
|
368
|
+
|
|
369
|
+
expect(() => {
|
|
370
|
+
instantiate(definition).withOptions({}).build();
|
|
371
|
+
}).not.toThrow();
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
describe("Service dependencies and composition", () => {
|
|
376
|
+
test("provided service can access used services", () => {
|
|
377
|
+
const emailImpl: IEmailService = {
|
|
378
|
+
sendEmail: async () => {},
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
const definition = defineFragment("test")
|
|
382
|
+
.usesService<"email", IEmailService>("email")
|
|
383
|
+
.providesBaseService(({ serviceDeps }) => ({
|
|
384
|
+
sendWelcomeEmail: async (to: string) => {
|
|
385
|
+
await serviceDeps.email.sendEmail(to, "Welcome", "Welcome to our service!");
|
|
386
|
+
},
|
|
387
|
+
}))
|
|
388
|
+
.build();
|
|
389
|
+
|
|
390
|
+
const instance = instantiate(definition)
|
|
391
|
+
.withOptions({})
|
|
392
|
+
.withServices({ email: emailImpl })
|
|
393
|
+
.build();
|
|
394
|
+
|
|
395
|
+
expect(instance.services.sendWelcomeEmail).toBeDefined();
|
|
396
|
+
expect(typeof instance.services.sendWelcomeEmail).toBe("function");
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
test("provided service can access used services - builder style", () => {
|
|
400
|
+
const emailImpl: IEmailService = {
|
|
401
|
+
sendEmail: async () => {},
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
const definition = defineFragment("test")
|
|
405
|
+
.usesService<"email", IEmailService>("email")
|
|
406
|
+
.providesBaseService(({ serviceDeps }) => ({
|
|
407
|
+
sendWelcomeEmail: async (to: string) => {
|
|
408
|
+
await serviceDeps.email.sendEmail(to, "Welcome", "Welcome to our service!");
|
|
409
|
+
},
|
|
410
|
+
}))
|
|
411
|
+
.build();
|
|
412
|
+
|
|
413
|
+
const instance = instantiate(definition)
|
|
414
|
+
.withServices({ email: emailImpl })
|
|
415
|
+
.withOptions({})
|
|
416
|
+
.build();
|
|
417
|
+
|
|
418
|
+
expect(instance.services.sendWelcomeEmail).toBeDefined();
|
|
419
|
+
expect(typeof instance.services.sendWelcomeEmail).toBe("function");
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
test("provided service can access config", () => {
|
|
423
|
+
const definition = defineFragment<{ apiKey: string }>("test")
|
|
424
|
+
.providesBaseService(({ config }) => ({
|
|
425
|
+
getApiKey: () => config.apiKey,
|
|
426
|
+
}))
|
|
427
|
+
.build();
|
|
428
|
+
|
|
429
|
+
const instance = instantiate(definition)
|
|
430
|
+
.withConfig({ apiKey: "test-key" })
|
|
431
|
+
.withOptions({})
|
|
432
|
+
.build();
|
|
433
|
+
|
|
434
|
+
expect(instance.services.getApiKey()).toBe("test-key");
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
test("provided service can access deps from withDependencies", () => {
|
|
438
|
+
const definition = defineFragment<{ apiKey: string }>("test")
|
|
439
|
+
.withDependencies(({ config }) => ({
|
|
440
|
+
client: { key: config.apiKey },
|
|
441
|
+
}))
|
|
442
|
+
.providesBaseService(({ deps }) => ({
|
|
443
|
+
getClient: () => deps.client,
|
|
444
|
+
}))
|
|
445
|
+
.build();
|
|
446
|
+
|
|
447
|
+
const instance = instantiate(definition)
|
|
448
|
+
.withConfig({ apiKey: "test-key" })
|
|
449
|
+
.withOptions({})
|
|
450
|
+
.build();
|
|
451
|
+
|
|
452
|
+
expect(instance.services.getClient()).toEqual({ key: "test-key" });
|
|
453
|
+
});
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
describe("Service chaining and multiple services", () => {
|
|
457
|
+
test("should support chaining multiple provided services", () => {
|
|
458
|
+
const definition = defineFragment("test")
|
|
459
|
+
.providesService("email", () => ({
|
|
460
|
+
sendEmail: async () => {},
|
|
461
|
+
}))
|
|
462
|
+
.providesService("logger", () => ({
|
|
463
|
+
log: () => {},
|
|
464
|
+
}))
|
|
465
|
+
.build();
|
|
466
|
+
|
|
467
|
+
const instance = instantiate(definition).withOptions({}).build();
|
|
468
|
+
expect(instance.services.email.sendEmail).toBeDefined();
|
|
469
|
+
expect(instance.services.logger.log).toBeDefined();
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
test("should support mixing unnamed and named provided services", () => {
|
|
473
|
+
const definition = defineFragment("test")
|
|
474
|
+
.providesBaseService(() => ({
|
|
475
|
+
helper: () => "help",
|
|
476
|
+
}))
|
|
477
|
+
.providesService("email", () => ({
|
|
478
|
+
sendEmail: async () => {},
|
|
479
|
+
}))
|
|
480
|
+
.build();
|
|
481
|
+
|
|
482
|
+
const instance = instantiate(definition).withOptions({}).build();
|
|
483
|
+
expect(instance.services.helper).toBeDefined();
|
|
484
|
+
expect(instance.services.email.sendEmail).toBeDefined();
|
|
485
|
+
});
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
describe("Optional service runtime behavior", () => {
|
|
489
|
+
test("should handle optional service when not provided", () => {
|
|
490
|
+
const definition = defineFragment("test")
|
|
491
|
+
.usesOptionalService<"email", IEmailService>("email")
|
|
492
|
+
.providesBaseService(({ serviceDeps }) => ({
|
|
493
|
+
maybeSendEmail: async (to: string) => {
|
|
494
|
+
if (serviceDeps.email) {
|
|
495
|
+
await serviceDeps.email.sendEmail(to, "Subject", "Body");
|
|
496
|
+
return true;
|
|
497
|
+
}
|
|
498
|
+
return false;
|
|
499
|
+
},
|
|
500
|
+
}))
|
|
501
|
+
.build();
|
|
502
|
+
|
|
503
|
+
const instance = instantiate(definition).withOptions({}).build();
|
|
504
|
+
|
|
505
|
+
expect(instance.services.maybeSendEmail).toBeDefined();
|
|
506
|
+
// Should not throw when optional service is not provided
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
test("should handle optional service when provided", () => {
|
|
510
|
+
const emailImpl: IEmailService = {
|
|
511
|
+
sendEmail: async () => {},
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
const definition = defineFragment("test")
|
|
515
|
+
.usesOptionalService<"email", IEmailService>("email")
|
|
516
|
+
.providesBaseService(({ serviceDeps }) => ({
|
|
517
|
+
maybeSendEmail: async (to: string) => {
|
|
518
|
+
if (serviceDeps.email) {
|
|
519
|
+
await serviceDeps.email.sendEmail(to, "Subject", "Body");
|
|
520
|
+
return true;
|
|
521
|
+
}
|
|
522
|
+
return false;
|
|
523
|
+
},
|
|
524
|
+
}))
|
|
525
|
+
.build();
|
|
526
|
+
|
|
527
|
+
const instance = instantiate(definition)
|
|
528
|
+
.withOptions({})
|
|
529
|
+
.withServices({ email: emailImpl })
|
|
530
|
+
.build();
|
|
531
|
+
|
|
532
|
+
expect(instance.services.maybeSendEmail).toBeDefined();
|
|
533
|
+
// When the optional service is provided, the wrapped method should work
|
|
534
|
+
expect(typeof instance.services.maybeSendEmail).toBe("function");
|
|
535
|
+
});
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
describe("Private Services", () => {
|
|
539
|
+
test("private service should be accessible when defining other services", () => {
|
|
540
|
+
interface IDataStore {
|
|
541
|
+
get(key: string): string | undefined;
|
|
542
|
+
set(key: string, value: string): void;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const dataStoreImpl: IDataStore = {
|
|
546
|
+
get: () => "cached-value",
|
|
547
|
+
set: () => {},
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
const definition = defineFragment("test")
|
|
551
|
+
.providesPrivateService("dataStore", () => dataStoreImpl)
|
|
552
|
+
.providesBaseService(({ privateServices }) => ({
|
|
553
|
+
getValue: (key: string) => {
|
|
554
|
+
// Private service is accessible here
|
|
555
|
+
return privateServices.dataStore.get(key);
|
|
556
|
+
},
|
|
557
|
+
}))
|
|
558
|
+
.build();
|
|
559
|
+
|
|
560
|
+
const instance = instantiate(definition).withOptions({}).build();
|
|
561
|
+
|
|
562
|
+
// Private service should NOT be accessible on the instance
|
|
563
|
+
expectTypeOf<typeof instance.services>().not.toMatchTypeOf<{ dataStore: IDataStore }>();
|
|
564
|
+
|
|
565
|
+
// But the public service that uses it should work
|
|
566
|
+
expect(instance.services.getValue).toBeDefined();
|
|
567
|
+
expect(instance.services.getValue("test")).toBe("cached-value");
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
test("private service should NOT be exposed on fragment instance", () => {
|
|
571
|
+
interface IInternalCache {
|
|
572
|
+
cache: Map<string, unknown>;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const definition = defineFragment("test")
|
|
576
|
+
.providesPrivateService<"cache", IInternalCache>("cache", () => ({
|
|
577
|
+
cache: new Map(),
|
|
578
|
+
}))
|
|
579
|
+
.providesBaseService(({ privateServices }) => ({
|
|
580
|
+
getCacheSize: () => privateServices.cache.cache.size,
|
|
581
|
+
}))
|
|
582
|
+
.build();
|
|
583
|
+
|
|
584
|
+
const instance = instantiate(definition).withOptions({}).build();
|
|
585
|
+
|
|
586
|
+
// @ts-expect-error - Private service should not be accessible
|
|
587
|
+
expect(instance.services.cache).toBeUndefined();
|
|
588
|
+
|
|
589
|
+
// Only the public service should be accessible
|
|
590
|
+
expect(instance.services.getCacheSize).toBeDefined();
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
test("multiple private services should work together", () => {
|
|
594
|
+
interface ILogger {
|
|
595
|
+
log(msg: string): void;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
interface ICache {
|
|
599
|
+
get(key: string): unknown;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const logger: ILogger = {
|
|
603
|
+
log: () => {},
|
|
604
|
+
};
|
|
605
|
+
|
|
606
|
+
const cache: ICache = {
|
|
607
|
+
get: () => "cached",
|
|
608
|
+
};
|
|
609
|
+
|
|
610
|
+
const definition = defineFragment("test")
|
|
611
|
+
.providesPrivateService("logger", () => logger)
|
|
612
|
+
.providesPrivateService("cache", () => cache)
|
|
613
|
+
.providesBaseService(({ privateServices }) => ({
|
|
614
|
+
getCachedValue: (key: string) => {
|
|
615
|
+
privateServices.logger.log(`Getting ${key}`);
|
|
616
|
+
return privateServices.cache.get(key);
|
|
617
|
+
},
|
|
618
|
+
}))
|
|
619
|
+
.build();
|
|
620
|
+
|
|
621
|
+
const instance = instantiate(definition).withOptions({}).build();
|
|
622
|
+
|
|
623
|
+
expect(instance.services.getCachedValue).toBeDefined();
|
|
624
|
+
expect(instance.services.getCachedValue("test")).toBe("cached");
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
test("private services can access config and deps", () => {
|
|
628
|
+
const definition = defineFragment<{ apiKey: string }>("test")
|
|
629
|
+
.withDependencies(({ config }) => ({
|
|
630
|
+
endpoint: `https://api.example.com/${config.apiKey}`,
|
|
631
|
+
}))
|
|
632
|
+
.providesPrivateService("internalApi", ({ deps }) => ({
|
|
633
|
+
makeRequest: () => `${deps.endpoint}/request`,
|
|
634
|
+
}))
|
|
635
|
+
.providesBaseService(({ privateServices }) => ({
|
|
636
|
+
doRequest: () => privateServices.internalApi.makeRequest(),
|
|
637
|
+
}))
|
|
638
|
+
.build();
|
|
639
|
+
|
|
640
|
+
const instance = instantiate(definition)
|
|
641
|
+
.withConfig({ apiKey: "test-key" })
|
|
642
|
+
.withOptions({})
|
|
643
|
+
.build();
|
|
644
|
+
|
|
645
|
+
expect(instance.services.doRequest()).toBe("https://api.example.com/test-key/request");
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
test("private services can access serviceDeps", () => {
|
|
649
|
+
interface IEmailService {
|
|
650
|
+
sendEmail(to: string): Promise<void>;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
const emailImpl: IEmailService = {
|
|
654
|
+
sendEmail: async () => {},
|
|
655
|
+
};
|
|
656
|
+
|
|
657
|
+
const definition = defineFragment("test")
|
|
658
|
+
.usesService<"email", IEmailService>("email")
|
|
659
|
+
.providesPrivateService("emailHelper", ({ serviceDeps }) => ({
|
|
660
|
+
sendWelcomeEmail: (to: string) => serviceDeps.email.sendEmail(to),
|
|
661
|
+
}))
|
|
662
|
+
.providesBaseService(({ privateServices }) => ({
|
|
663
|
+
welcomeUser: (email: string) => privateServices.emailHelper.sendWelcomeEmail(email),
|
|
664
|
+
}))
|
|
665
|
+
.build();
|
|
666
|
+
|
|
667
|
+
const instance = instantiate(definition)
|
|
668
|
+
.withServices({ email: emailImpl })
|
|
669
|
+
.withOptions({})
|
|
670
|
+
.build();
|
|
671
|
+
|
|
672
|
+
expect(instance.services.welcomeUser).toBeDefined();
|
|
673
|
+
expectTypeOf<typeof instance.services.welcomeUser>().toExtend<
|
|
674
|
+
(email: string) => Promise<void>
|
|
675
|
+
>();
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
test("named services can also access private services", () => {
|
|
679
|
+
const definition = defineFragment("test")
|
|
680
|
+
.providesPrivateService("helper", () => ({
|
|
681
|
+
multiply: (a: number, b: number) => a * b,
|
|
682
|
+
}))
|
|
683
|
+
.providesService("calculator", ({ privateServices }) => ({
|
|
684
|
+
square: (n: number) => privateServices.helper.multiply(n, n),
|
|
685
|
+
}))
|
|
686
|
+
.build();
|
|
687
|
+
|
|
688
|
+
const instance = instantiate(definition).withOptions({}).build();
|
|
689
|
+
|
|
690
|
+
expect(instance.services.calculator.square(5)).toBe(25);
|
|
691
|
+
// @ts-expect-error - Private service should not be accessible
|
|
692
|
+
expect(instance.services.helper).toBeUndefined();
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
test("private services can access other private services (in order)", () => {
|
|
696
|
+
const definition = defineFragment("test")
|
|
697
|
+
.providesPrivateService("math", () => ({
|
|
698
|
+
add: (a: number, b: number) => a + b,
|
|
699
|
+
multiply: (a: number, b: number) => a * b,
|
|
700
|
+
}))
|
|
701
|
+
.providesPrivateService("calculator", ({ privateServices }) => ({
|
|
702
|
+
// This private service can access the earlier private service
|
|
703
|
+
square: (n: number) => privateServices.math.multiply(n, n),
|
|
704
|
+
addTen: (n: number) => privateServices.math.add(n, 10),
|
|
705
|
+
}))
|
|
706
|
+
.providesBaseService(({ privateServices }) => ({
|
|
707
|
+
compute: (n: number) => {
|
|
708
|
+
// Public service can access both private services
|
|
709
|
+
const squared = privateServices.calculator.square(n);
|
|
710
|
+
return privateServices.calculator.addTen(squared);
|
|
711
|
+
},
|
|
712
|
+
}))
|
|
713
|
+
.build();
|
|
714
|
+
|
|
715
|
+
const instance = instantiate(definition).withOptions({}).build();
|
|
716
|
+
|
|
717
|
+
// 5^2 = 25, 25 + 10 = 35
|
|
718
|
+
expect(instance.services.compute(5)).toBe(35);
|
|
719
|
+
|
|
720
|
+
// Private services should not be accessible on the instance
|
|
721
|
+
// @ts-expect-error - Private service should not be accessible
|
|
722
|
+
expect(instance.services.math).toBeUndefined();
|
|
723
|
+
// @ts-expect-error - Private service should not be accessible
|
|
724
|
+
expect(instance.services.calculator).toBeUndefined();
|
|
725
|
+
});
|
|
726
|
+
});
|
|
727
|
+
});
|