@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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { test, expect, describe } from "vitest";
|
|
2
2
|
import { defineFragment } from "./fragment-builder";
|
|
3
|
-
import { createFragment } from "./fragment-instantiation";
|
|
3
|
+
import { createFragment, instantiateFragment } from "./fragment-instantiation";
|
|
4
4
|
import { defineRoute, defineRoutes } from "./route";
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
|
|
@@ -31,7 +31,7 @@ describe("callRoute", () => {
|
|
|
31
31
|
body: { name: "World" },
|
|
32
32
|
});
|
|
33
33
|
|
|
34
|
-
expect(response.type).toBe("json");
|
|
34
|
+
// expect(response.type).toBe("json");
|
|
35
35
|
if (response.type === "json") {
|
|
36
36
|
expect(response.status).toBe(200);
|
|
37
37
|
expect(response.data).toEqual({ message: "Hello, World!" });
|
|
@@ -391,11 +391,13 @@ describe("callRoute", () => {
|
|
|
391
391
|
getUserName: () => string;
|
|
392
392
|
};
|
|
393
393
|
|
|
394
|
-
const fragment = defineFragment<typeof config>("test-fragment").
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
394
|
+
const fragment = defineFragment<typeof config>("test-fragment").providesService(
|
|
395
|
+
({ defineService }) => {
|
|
396
|
+
return defineService({
|
|
397
|
+
getUserName: () => "Test User",
|
|
398
|
+
});
|
|
399
|
+
},
|
|
400
|
+
);
|
|
399
401
|
|
|
400
402
|
const routesFactory = defineRoutes<typeof config, {}, Services>().create(({ services }) => {
|
|
401
403
|
return [
|
|
@@ -457,4 +459,244 @@ describe("callRoute", () => {
|
|
|
457
459
|
expect(response.data).toEqual({ result: "database-result" });
|
|
458
460
|
}
|
|
459
461
|
});
|
|
462
|
+
|
|
463
|
+
test("this context is RequestThisContext for standard fragments", async () => {
|
|
464
|
+
const config = {};
|
|
465
|
+
|
|
466
|
+
const fragment = defineFragment<typeof config>("test-fragment");
|
|
467
|
+
|
|
468
|
+
const routesFactory = defineRoutes<typeof config>().create(() => {
|
|
469
|
+
return [
|
|
470
|
+
defineRoute({
|
|
471
|
+
method: "GET",
|
|
472
|
+
path: "/context-test",
|
|
473
|
+
outputSchema: z.object({ contextType: z.string() }),
|
|
474
|
+
handler: async function (_, { json }) {
|
|
475
|
+
// this should be RequestThisContext (empty object by default)
|
|
476
|
+
expect(this).toBeDefined();
|
|
477
|
+
expect(typeof this).toBe("object");
|
|
478
|
+
return json({ contextType: "standard" });
|
|
479
|
+
},
|
|
480
|
+
}),
|
|
481
|
+
];
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
const instance = createFragment(fragment, config, [routesFactory], {});
|
|
485
|
+
|
|
486
|
+
const response = await instance.callRoute("GET", "/context-test");
|
|
487
|
+
|
|
488
|
+
expect(response.type).toBe("json");
|
|
489
|
+
if (response.type === "json") {
|
|
490
|
+
expect(response.status).toBe(200);
|
|
491
|
+
expect(response.data).toEqual({ contextType: "standard" });
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
test("route handlers receive correct this context at runtime", async () => {
|
|
496
|
+
const fragment = defineFragment("this-test");
|
|
497
|
+
|
|
498
|
+
const routesFactory = defineRoutes(fragment).create(({ defineRoute }) => {
|
|
499
|
+
return [
|
|
500
|
+
defineRoute({
|
|
501
|
+
method: "GET",
|
|
502
|
+
path: "/this-test",
|
|
503
|
+
outputSchema: z.object({ hasThis: z.boolean() }),
|
|
504
|
+
handler: async function (_, { json }) {
|
|
505
|
+
// Verify that 'this' is defined and is an object
|
|
506
|
+
const hasThis = this !== undefined && typeof this === "object";
|
|
507
|
+
return json({ hasThis });
|
|
508
|
+
},
|
|
509
|
+
}),
|
|
510
|
+
];
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
const instance = createFragment(fragment, {}, [routesFactory], {});
|
|
514
|
+
|
|
515
|
+
const response = await instance.callRoute("GET", "/this-test");
|
|
516
|
+
|
|
517
|
+
expect(response.type).toBe("json");
|
|
518
|
+
if (response.type === "json") {
|
|
519
|
+
expect(response.status).toBe(200);
|
|
520
|
+
expect(response.data).toEqual({ hasThis: true });
|
|
521
|
+
}
|
|
522
|
+
});
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
describe("FragmentInstantiationBuilder", () => {
|
|
526
|
+
describe("instantiate", () => {
|
|
527
|
+
test("basic instantiation with defaults", () => {
|
|
528
|
+
const fragment = defineFragment<{ apiKey: string }>("test");
|
|
529
|
+
|
|
530
|
+
const instance = instantiateFragment(fragment).build();
|
|
531
|
+
|
|
532
|
+
expect(instance.config.name).toBe("test");
|
|
533
|
+
expect(instance.deps).toEqual({});
|
|
534
|
+
expect(instance.services).toEqual({});
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
test("with config", () => {
|
|
538
|
+
const fragment = defineFragment<{ apiKey: string }>("test");
|
|
539
|
+
|
|
540
|
+
const instance = instantiateFragment(fragment).withConfig({ apiKey: "test-key" }).build();
|
|
541
|
+
|
|
542
|
+
expect(instance.config.name).toBe("test");
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
test("with routes", async () => {
|
|
546
|
+
const fragment = defineFragment<{}>("test");
|
|
547
|
+
|
|
548
|
+
const route = defineRoute({
|
|
549
|
+
method: "GET",
|
|
550
|
+
path: "/hello",
|
|
551
|
+
outputSchema: z.object({ message: z.string() }),
|
|
552
|
+
handler: async (_ctx, { json }) => json({ message: "hello" }),
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
const instance = instantiateFragment(fragment).withConfig({}).withRoutes([route]).build();
|
|
556
|
+
|
|
557
|
+
const response = await instance.callRoute("GET", "/hello");
|
|
558
|
+
|
|
559
|
+
expect(response.type).toBe("json");
|
|
560
|
+
if (response.type === "json") {
|
|
561
|
+
expect(response.data).toEqual({ message: "hello" });
|
|
562
|
+
}
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
test("with options", () => {
|
|
566
|
+
const fragment = defineFragment<{}>("test");
|
|
567
|
+
|
|
568
|
+
const instance = instantiateFragment(fragment)
|
|
569
|
+
.withConfig({})
|
|
570
|
+
.withOptions({ mountRoute: "/custom" })
|
|
571
|
+
.build();
|
|
572
|
+
|
|
573
|
+
expect(instance.mountRoute).toBe("/custom");
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
test("with services (used services)", () => {
|
|
577
|
+
interface IEmailService {
|
|
578
|
+
sendEmail(to: string, subject: string): Promise<void>;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
const emailImpl: IEmailService = {
|
|
582
|
+
sendEmail: async () => {},
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
const fragment = defineFragment<{}>("test").usesService<"email", IEmailService>("email");
|
|
586
|
+
|
|
587
|
+
const instance = instantiateFragment(fragment)
|
|
588
|
+
.withConfig({})
|
|
589
|
+
.withServices({ email: emailImpl })
|
|
590
|
+
.build();
|
|
591
|
+
|
|
592
|
+
expect(instance.services.email).toBeDefined();
|
|
593
|
+
expect(instance.services.email.sendEmail).toBeDefined();
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
test("all options together", async () => {
|
|
597
|
+
interface ILogger {
|
|
598
|
+
log(message: string): void;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const loggerImpl: ILogger = {
|
|
602
|
+
log: () => {},
|
|
603
|
+
};
|
|
604
|
+
|
|
605
|
+
const fragment = defineFragment<{ apiKey: string }>("test")
|
|
606
|
+
.withDependencies(({ config }) => ({
|
|
607
|
+
client: { key: config.apiKey },
|
|
608
|
+
}))
|
|
609
|
+
.usesService<"logger", ILogger>("logger");
|
|
610
|
+
|
|
611
|
+
const route = defineRoute({
|
|
612
|
+
method: "GET",
|
|
613
|
+
path: "/test",
|
|
614
|
+
outputSchema: z.object({ success: z.boolean() }),
|
|
615
|
+
handler: async (_ctx, { json }) => json({ success: true }),
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
const instance = instantiateFragment(fragment)
|
|
619
|
+
.withConfig({ apiKey: "my-key" })
|
|
620
|
+
.withRoutes([route])
|
|
621
|
+
.withOptions({ mountRoute: "/api" })
|
|
622
|
+
.withServices({ logger: loggerImpl })
|
|
623
|
+
.build();
|
|
624
|
+
|
|
625
|
+
expect(instance.config.name).toBe("test");
|
|
626
|
+
expect(instance.mountRoute).toBe("/api");
|
|
627
|
+
expect(instance.deps.client.key).toBe("my-key");
|
|
628
|
+
expect(instance.services.logger).toBeDefined();
|
|
629
|
+
|
|
630
|
+
const response = await instance.callRoute("GET", "/test");
|
|
631
|
+
expect(response.type).toBe("json");
|
|
632
|
+
if (response.type === "json") {
|
|
633
|
+
expect(response.data).toEqual({ success: true });
|
|
634
|
+
}
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
test("method chaining is fluent", () => {
|
|
638
|
+
const fragment = defineFragment<{ apiKey: string }>("test");
|
|
639
|
+
|
|
640
|
+
// Should be chainable
|
|
641
|
+
const builder = instantiateFragment(fragment)
|
|
642
|
+
.withConfig({ apiKey: "key" })
|
|
643
|
+
.withRoutes([])
|
|
644
|
+
.withOptions({});
|
|
645
|
+
|
|
646
|
+
expect(builder).toBeDefined();
|
|
647
|
+
expect(typeof builder.build).toBe("function");
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
test("config defaults to empty object", () => {
|
|
651
|
+
const fragment = defineFragment<{}>("test");
|
|
652
|
+
|
|
653
|
+
const instance = instantiateFragment(fragment).build();
|
|
654
|
+
|
|
655
|
+
expect(instance).toBeDefined();
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
test("routes defaults to empty array", () => {
|
|
659
|
+
const fragment = defineFragment<{}>("test");
|
|
660
|
+
|
|
661
|
+
const instance = instantiateFragment(fragment).withConfig({}).build();
|
|
662
|
+
|
|
663
|
+
expect(instance.config.routes).toEqual([]);
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
test("options defaults to empty object", () => {
|
|
667
|
+
const fragment = defineFragment<{}>("test");
|
|
668
|
+
|
|
669
|
+
const instance = instantiateFragment(fragment).withConfig({}).build();
|
|
670
|
+
|
|
671
|
+
// Default mountRoute is generated
|
|
672
|
+
expect(instance.mountRoute).toContain("/test");
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
test("services are optional if not required", () => {
|
|
676
|
+
interface ILogger {
|
|
677
|
+
log(message: string): void;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
const fragment = defineFragment<{}>("test").usesService<"logger", ILogger>("logger", {
|
|
681
|
+
optional: true,
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
// Should not throw even without providing the service
|
|
685
|
+
const instance = instantiateFragment(fragment).withConfig({}).build();
|
|
686
|
+
|
|
687
|
+
expect(instance).toBeDefined();
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
test("throws when required service not provided", () => {
|
|
691
|
+
interface IEmailService {
|
|
692
|
+
sendEmail(to: string): Promise<void>;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
const fragment = defineFragment<{}>("test").usesService<"email", IEmailService>("email");
|
|
696
|
+
|
|
697
|
+
expect(() => {
|
|
698
|
+
instantiateFragment(fragment).withConfig({}).build();
|
|
699
|
+
}).toThrow("Fragment 'test' requires service 'email' but it was not provided");
|
|
700
|
+
});
|
|
701
|
+
});
|
|
460
702
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { StandardSchemaV1 } from "@standard-schema/spec";
|
|
2
|
-
import { type FragnoRouteConfig, type HTTPMethod } from "./api";
|
|
2
|
+
import { type FragnoRouteConfig, type HTTPMethod, type RequestThisContext } from "./api";
|
|
3
3
|
import { FragnoApiError } from "./error";
|
|
4
4
|
import { getMountRoute } from "./internal/route";
|
|
5
5
|
import { addRoute, createRouter, findRoute } from "rou3";
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
RequestMiddlewareOutputContext,
|
|
18
18
|
type FragnoMiddlewareCallback,
|
|
19
19
|
} from "./request-middleware";
|
|
20
|
-
import type { FragmentDefinition } from "./fragment-builder";
|
|
20
|
+
import type { FragmentDefinition, RouteHandler } from "./fragment-builder";
|
|
21
21
|
import { MutableRequestState } from "./mutable-request-state";
|
|
22
22
|
import type { RouteHandlerInputOptions } from "./route-handler-input-options";
|
|
23
23
|
import type { ExtractRouteByPath, ExtractRoutePath } from "../client/client";
|
|
@@ -147,29 +147,94 @@ export function createFragment<
|
|
|
147
147
|
const TServices extends Record<string, unknown>,
|
|
148
148
|
const TRoutesOrFactories extends readonly AnyRouteOrFactory[],
|
|
149
149
|
const TAdditionalContext extends Record<string, unknown>,
|
|
150
|
+
const TRequiredInterfaces extends Record<string, unknown>,
|
|
151
|
+
const TProvidedInterfaces extends Record<string, unknown>,
|
|
150
152
|
const TOptions extends FragnoPublicConfig,
|
|
153
|
+
const TThisContext extends RequestThisContext = RequestThisContext,
|
|
151
154
|
>(
|
|
152
155
|
fragmentBuilder: {
|
|
153
|
-
definition: FragmentDefinition<
|
|
156
|
+
definition: FragmentDefinition<
|
|
157
|
+
TConfig,
|
|
158
|
+
TDeps,
|
|
159
|
+
TServices,
|
|
160
|
+
TAdditionalContext,
|
|
161
|
+
TRequiredInterfaces,
|
|
162
|
+
TProvidedInterfaces,
|
|
163
|
+
TThisContext
|
|
164
|
+
>;
|
|
154
165
|
$requiredOptions: TOptions;
|
|
155
166
|
},
|
|
156
167
|
config: TConfig,
|
|
157
168
|
routesOrFactories: TRoutesOrFactories,
|
|
158
169
|
options: TOptions,
|
|
170
|
+
interfaceImplementations?: TRequiredInterfaces,
|
|
159
171
|
): FragnoInstantiatedFragment<
|
|
160
172
|
FlattenRouteFactories<TRoutesOrFactories>,
|
|
161
|
-
TDeps,
|
|
162
|
-
TServices,
|
|
173
|
+
TDeps & TRequiredInterfaces,
|
|
174
|
+
TServices & TProvidedInterfaces & TRequiredInterfaces,
|
|
163
175
|
TAdditionalContext
|
|
164
176
|
> {
|
|
165
177
|
type TRoutes = FlattenRouteFactories<TRoutesOrFactories>;
|
|
166
178
|
|
|
167
179
|
const definition = fragmentBuilder.definition;
|
|
168
180
|
|
|
181
|
+
// Validate required services are satisfied
|
|
182
|
+
if (definition.usedServices) {
|
|
183
|
+
for (const [serviceName, serviceMeta] of Object.entries(definition.usedServices)) {
|
|
184
|
+
const implementation = interfaceImplementations?.[serviceName];
|
|
185
|
+
if (serviceMeta.required && !implementation) {
|
|
186
|
+
throw new Error(
|
|
187
|
+
`Fragment '${definition.name}' requires service '${serviceMeta.name}' but it was not provided`,
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
169
193
|
const dependencies = definition.dependencies?.(config, options) ?? ({} as TDeps);
|
|
170
|
-
const services = definition.services?.(config, options, dependencies) ?? ({} as TServices);
|
|
171
194
|
|
|
172
|
-
|
|
195
|
+
// Merge interface implementations into dependencies
|
|
196
|
+
const depsWithInterfaces = {
|
|
197
|
+
...dependencies,
|
|
198
|
+
...interfaceImplementations,
|
|
199
|
+
} as TDeps & TRequiredInterfaces;
|
|
200
|
+
|
|
201
|
+
const servicesFromWithServices =
|
|
202
|
+
definition.services?.(config, options, depsWithInterfaces) ?? ({} as TServices);
|
|
203
|
+
|
|
204
|
+
// Handle providedServices - can be:
|
|
205
|
+
// 1. A function that returns all services
|
|
206
|
+
// 2. An object where each value is a factory function
|
|
207
|
+
// 3. undefined
|
|
208
|
+
let providedServicesResolved: TProvidedInterfaces | undefined;
|
|
209
|
+
|
|
210
|
+
if (typeof definition.providedServices === "function") {
|
|
211
|
+
// Case 1: It's a function, call it to get the services
|
|
212
|
+
providedServicesResolved = definition.providedServices(config, options, depsWithInterfaces);
|
|
213
|
+
} else if (definition.providedServices && typeof definition.providedServices === "object") {
|
|
214
|
+
// Case 2: It's an object where each value might be a factory function
|
|
215
|
+
providedServicesResolved = {} as TProvidedInterfaces;
|
|
216
|
+
for (const [serviceName, serviceOrFactory] of Object.entries(definition.providedServices)) {
|
|
217
|
+
if (typeof serviceOrFactory === "function") {
|
|
218
|
+
// Call the factory function
|
|
219
|
+
(providedServicesResolved as Record<string, unknown>)[serviceName] = serviceOrFactory(
|
|
220
|
+
config,
|
|
221
|
+
options,
|
|
222
|
+
depsWithInterfaces,
|
|
223
|
+
);
|
|
224
|
+
} else {
|
|
225
|
+
// It's already a resolved service
|
|
226
|
+
(providedServicesResolved as Record<string, unknown>)[serviceName] = serviceOrFactory;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const services = {
|
|
232
|
+
...servicesFromWithServices,
|
|
233
|
+
...providedServicesResolved,
|
|
234
|
+
...interfaceImplementations,
|
|
235
|
+
} as TServices & TProvidedInterfaces & TRequiredInterfaces;
|
|
236
|
+
|
|
237
|
+
const context = { config, deps: depsWithInterfaces, services };
|
|
173
238
|
const routes = resolveRouteFactories(context, routesOrFactories);
|
|
174
239
|
|
|
175
240
|
const mountRoute = getMountRoute({
|
|
@@ -185,22 +250,30 @@ export function createFragment<
|
|
|
185
250
|
StandardSchemaV1 | undefined,
|
|
186
251
|
StandardSchemaV1 | undefined,
|
|
187
252
|
string,
|
|
188
|
-
string
|
|
253
|
+
string,
|
|
254
|
+
RequestThisContext
|
|
189
255
|
>
|
|
190
256
|
>();
|
|
191
257
|
|
|
192
258
|
let middlewareHandler:
|
|
193
|
-
| FragnoMiddlewareCallback<
|
|
259
|
+
| FragnoMiddlewareCallback<
|
|
260
|
+
FlattenRouteFactories<TRoutesOrFactories>,
|
|
261
|
+
TDeps & TRequiredInterfaces,
|
|
262
|
+
TServices & TProvidedInterfaces & TRequiredInterfaces
|
|
263
|
+
>
|
|
194
264
|
| undefined;
|
|
195
265
|
|
|
266
|
+
// Store the handler wrapper if provided (e.g., for database support)
|
|
267
|
+
const handlerWrapper = definition.createHandlerWrapper?.(options);
|
|
268
|
+
|
|
196
269
|
for (const routeConfig of routes) {
|
|
197
270
|
addRoute(router, routeConfig.method.toUpperCase(), routeConfig.path, routeConfig);
|
|
198
271
|
}
|
|
199
272
|
|
|
200
273
|
const fragment: FragnoInstantiatedFragment<
|
|
201
274
|
FlattenRouteFactories<TRoutesOrFactories>,
|
|
202
|
-
TDeps,
|
|
203
|
-
TServices,
|
|
275
|
+
TDeps & TRequiredInterfaces,
|
|
276
|
+
TServices & TProvidedInterfaces & TRequiredInterfaces,
|
|
204
277
|
TAdditionalContext & TOptions
|
|
205
278
|
> = {
|
|
206
279
|
[instantiatedFragmentFakeSymbol]: instantiatedFragmentFakeSymbol,
|
|
@@ -210,7 +283,7 @@ export function createFragment<
|
|
|
210
283
|
routes,
|
|
211
284
|
},
|
|
212
285
|
services,
|
|
213
|
-
deps:
|
|
286
|
+
deps: depsWithInterfaces,
|
|
214
287
|
additionalContext: {
|
|
215
288
|
...definition.additionalContext,
|
|
216
289
|
...options,
|
|
@@ -297,9 +370,25 @@ export function createFragment<
|
|
|
297
370
|
// Construct RequestOutputContext
|
|
298
371
|
const outputContext = new RequestOutputContext(route.outputSchema);
|
|
299
372
|
|
|
300
|
-
// Call the route handler
|
|
373
|
+
// Call the route handler (wrap with handlerWrapper if provided)
|
|
301
374
|
try {
|
|
302
|
-
|
|
375
|
+
let response: Response;
|
|
376
|
+
const thisContext: RequestThisContext = {};
|
|
377
|
+
|
|
378
|
+
if (handlerWrapper) {
|
|
379
|
+
// Wrapper handles binding the this context internally for database fragments
|
|
380
|
+
// Safe: wrapper knows how to handle the specific this type (DatabaseRequestThisContext)
|
|
381
|
+
const wrappedHandler = handlerWrapper(route.handler as unknown as RouteHandler);
|
|
382
|
+
response = await wrappedHandler.call(thisContext, inputContext, outputContext);
|
|
383
|
+
} else {
|
|
384
|
+
// For standard fragments, bind to an empty RequestThisContext
|
|
385
|
+
// Safe: we know route.handler expects RequestThisContext for standard fragments
|
|
386
|
+
response = await (route.handler as RouteHandler).call(
|
|
387
|
+
thisContext,
|
|
388
|
+
inputContext,
|
|
389
|
+
outputContext,
|
|
390
|
+
);
|
|
391
|
+
}
|
|
303
392
|
return response;
|
|
304
393
|
} catch (error) {
|
|
305
394
|
console.error("Error in callRoute handler", error);
|
|
@@ -440,7 +529,10 @@ export function createFragment<
|
|
|
440
529
|
state: requestState,
|
|
441
530
|
});
|
|
442
531
|
|
|
443
|
-
const middlewareOutputContext = new RequestMiddlewareOutputContext(
|
|
532
|
+
const middlewareOutputContext = new RequestMiddlewareOutputContext(
|
|
533
|
+
depsWithInterfaces,
|
|
534
|
+
services,
|
|
535
|
+
);
|
|
444
536
|
|
|
445
537
|
try {
|
|
446
538
|
const middlewareResult = await middlewareHandler(
|
|
@@ -478,7 +570,16 @@ export function createFragment<
|
|
|
478
570
|
});
|
|
479
571
|
|
|
480
572
|
try {
|
|
481
|
-
|
|
573
|
+
// Apply handler wrapper if provided (e.g., for database support)
|
|
574
|
+
// Safe cast: handler wrapper preserves handler signature
|
|
575
|
+
const actualHandler = handlerWrapper
|
|
576
|
+
? (handlerWrapper(handler as RouteHandler) as typeof handler)
|
|
577
|
+
: handler;
|
|
578
|
+
|
|
579
|
+
// Create base this context (empty object for standard fragments)
|
|
580
|
+
// Database fragments will provide their own context via handler wrapper
|
|
581
|
+
const thisContext = {} as RequestThisContext;
|
|
582
|
+
const result = await actualHandler.call(thisContext, inputContext, outputContext);
|
|
482
583
|
return result;
|
|
483
584
|
} catch (error) {
|
|
484
585
|
console.error("Error in handler", error);
|
|
@@ -497,3 +598,169 @@ export function createFragment<
|
|
|
497
598
|
|
|
498
599
|
return fragment;
|
|
499
600
|
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Builder class for fluent fragment instantiation API
|
|
604
|
+
*/
|
|
605
|
+
export class FragmentInstantiationBuilder<
|
|
606
|
+
TConfig,
|
|
607
|
+
TDeps,
|
|
608
|
+
TServices extends Record<string, unknown>,
|
|
609
|
+
TRoutesOrFactories extends readonly AnyRouteOrFactory[],
|
|
610
|
+
TAdditionalContext extends Record<string, unknown>,
|
|
611
|
+
TRequiredInterfaces extends Record<string, unknown>,
|
|
612
|
+
TProvidedInterfaces extends Record<string, unknown>,
|
|
613
|
+
TOptions extends FragnoPublicConfig,
|
|
614
|
+
TThisContext extends RequestThisContext,
|
|
615
|
+
> {
|
|
616
|
+
#fragmentBuilder: {
|
|
617
|
+
definition: FragmentDefinition<
|
|
618
|
+
TConfig,
|
|
619
|
+
TDeps,
|
|
620
|
+
TServices,
|
|
621
|
+
TAdditionalContext,
|
|
622
|
+
TRequiredInterfaces,
|
|
623
|
+
TProvidedInterfaces,
|
|
624
|
+
TThisContext
|
|
625
|
+
>;
|
|
626
|
+
$requiredOptions: TOptions;
|
|
627
|
+
};
|
|
628
|
+
#config?: TConfig;
|
|
629
|
+
#routes?: TRoutesOrFactories;
|
|
630
|
+
#options?: TOptions;
|
|
631
|
+
#services?: TRequiredInterfaces;
|
|
632
|
+
|
|
633
|
+
constructor(fragmentBuilder: {
|
|
634
|
+
definition: FragmentDefinition<
|
|
635
|
+
TConfig,
|
|
636
|
+
TDeps,
|
|
637
|
+
TServices,
|
|
638
|
+
TAdditionalContext,
|
|
639
|
+
TRequiredInterfaces,
|
|
640
|
+
TProvidedInterfaces,
|
|
641
|
+
TThisContext
|
|
642
|
+
>;
|
|
643
|
+
$requiredOptions: TOptions;
|
|
644
|
+
}) {
|
|
645
|
+
this.#fragmentBuilder = fragmentBuilder;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* Set the configuration for the fragment
|
|
650
|
+
*/
|
|
651
|
+
withConfig(config: TConfig): this {
|
|
652
|
+
this.#config = config;
|
|
653
|
+
return this;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* Set the routes for the fragment
|
|
658
|
+
*/
|
|
659
|
+
withRoutes<const TNewRoutes extends readonly AnyRouteOrFactory[]>(
|
|
660
|
+
routes: TNewRoutes,
|
|
661
|
+
): FragmentInstantiationBuilder<
|
|
662
|
+
TConfig,
|
|
663
|
+
TDeps,
|
|
664
|
+
TServices,
|
|
665
|
+
TNewRoutes,
|
|
666
|
+
TAdditionalContext,
|
|
667
|
+
TRequiredInterfaces,
|
|
668
|
+
TProvidedInterfaces,
|
|
669
|
+
TOptions,
|
|
670
|
+
TThisContext
|
|
671
|
+
> {
|
|
672
|
+
this.#routes = routes as unknown as TRoutesOrFactories;
|
|
673
|
+
// Safe cast: We're changing the route type parameter
|
|
674
|
+
return this as unknown as FragmentInstantiationBuilder<
|
|
675
|
+
TConfig,
|
|
676
|
+
TDeps,
|
|
677
|
+
TServices,
|
|
678
|
+
TNewRoutes,
|
|
679
|
+
TAdditionalContext,
|
|
680
|
+
TRequiredInterfaces,
|
|
681
|
+
TProvidedInterfaces,
|
|
682
|
+
TOptions,
|
|
683
|
+
TThisContext
|
|
684
|
+
>;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* Set the options for the fragment (e.g., mountRoute, databaseAdapter)
|
|
689
|
+
*/
|
|
690
|
+
withOptions(options: TOptions): this {
|
|
691
|
+
this.#options = options;
|
|
692
|
+
return this;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* Provide implementations for services that this fragment uses
|
|
697
|
+
*/
|
|
698
|
+
withServices(services: TRequiredInterfaces): this {
|
|
699
|
+
this.#services = services;
|
|
700
|
+
return this;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* Build and return the instantiated fragment
|
|
705
|
+
*/
|
|
706
|
+
build(): FragnoInstantiatedFragment<
|
|
707
|
+
FlattenRouteFactories<TRoutesOrFactories>,
|
|
708
|
+
TDeps & TRequiredInterfaces,
|
|
709
|
+
TServices & TProvidedInterfaces & TRequiredInterfaces,
|
|
710
|
+
TAdditionalContext
|
|
711
|
+
> {
|
|
712
|
+
return createFragment(
|
|
713
|
+
this.#fragmentBuilder,
|
|
714
|
+
this.#config ?? ({} as TConfig),
|
|
715
|
+
this.#routes ?? ([] as const as unknown as TRoutesOrFactories),
|
|
716
|
+
this.#options ?? ({} as TOptions),
|
|
717
|
+
this.#services,
|
|
718
|
+
);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* Create a fluent builder for instantiating a fragment
|
|
724
|
+
*
|
|
725
|
+
* @example
|
|
726
|
+
* ```ts
|
|
727
|
+
* const fragment = instantiateFragment(myFragmentBuilder)
|
|
728
|
+
* .withConfig({ apiKey: "key" })
|
|
729
|
+
* .withRoutes([route1, route2])
|
|
730
|
+
* .withOptions({ mountRoute: "/api" })
|
|
731
|
+
* .build();
|
|
732
|
+
* ```
|
|
733
|
+
*/
|
|
734
|
+
export function instantiateFragment<
|
|
735
|
+
TConfig,
|
|
736
|
+
TDeps,
|
|
737
|
+
TServices extends Record<string, unknown>,
|
|
738
|
+
TAdditionalContext extends Record<string, unknown>,
|
|
739
|
+
TRequiredInterfaces extends Record<string, unknown>,
|
|
740
|
+
TProvidedInterfaces extends Record<string, unknown>,
|
|
741
|
+
TOptions extends FragnoPublicConfig,
|
|
742
|
+
TThisContext extends RequestThisContext = RequestThisContext,
|
|
743
|
+
>(fragmentBuilder: {
|
|
744
|
+
definition: FragmentDefinition<
|
|
745
|
+
TConfig,
|
|
746
|
+
TDeps,
|
|
747
|
+
TServices,
|
|
748
|
+
TAdditionalContext,
|
|
749
|
+
TRequiredInterfaces,
|
|
750
|
+
TProvidedInterfaces,
|
|
751
|
+
TThisContext
|
|
752
|
+
>;
|
|
753
|
+
$requiredOptions: TOptions;
|
|
754
|
+
}): FragmentInstantiationBuilder<
|
|
755
|
+
TConfig,
|
|
756
|
+
TDeps,
|
|
757
|
+
TServices,
|
|
758
|
+
readonly [],
|
|
759
|
+
TAdditionalContext,
|
|
760
|
+
TRequiredInterfaces,
|
|
761
|
+
TProvidedInterfaces,
|
|
762
|
+
TOptions,
|
|
763
|
+
TThisContext
|
|
764
|
+
> {
|
|
765
|
+
return new FragmentInstantiationBuilder(fragmentBuilder);
|
|
766
|
+
}
|