@fragno-dev/core 0.1.11 → 0.2.2
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 +87 -69
- package/CHANGELOG.md +79 -0
- package/dist/api/api.d.ts +21 -2
- package/dist/api/api.d.ts.map +1 -1
- package/dist/api/api.js +2 -1
- package/dist/api/api.js.map +1 -1
- package/dist/api/bind-services.d.ts +0 -1
- package/dist/api/bind-services.d.ts.map +1 -1
- package/dist/api/bind-services.js.map +1 -1
- package/dist/api/error.d.ts.map +1 -1
- package/dist/api/error.js.map +1 -1
- package/dist/api/fragment-definition-builder.d.ts +32 -40
- package/dist/api/fragment-definition-builder.d.ts.map +1 -1
- package/dist/api/fragment-definition-builder.js +15 -21
- package/dist/api/fragment-definition-builder.js.map +1 -1
- package/dist/api/fragment-instantiator.d.ts +51 -30
- package/dist/api/fragment-instantiator.d.ts.map +1 -1
- package/dist/api/fragment-instantiator.js +201 -52
- package/dist/api/fragment-instantiator.js.map +1 -1
- package/dist/api/request-context-storage.d.ts +4 -0
- package/dist/api/request-context-storage.d.ts.map +1 -1
- package/dist/api/request-context-storage.js +6 -0
- package/dist/api/request-context-storage.js.map +1 -1
- package/dist/api/request-input-context.d.ts +57 -1
- package/dist/api/request-input-context.d.ts.map +1 -1
- package/dist/api/request-input-context.js +67 -0
- package/dist/api/request-input-context.js.map +1 -1
- package/dist/api/request-middleware.d.ts +2 -2
- package/dist/api/request-middleware.d.ts.map +1 -1
- package/dist/api/request-middleware.js.map +1 -1
- package/dist/api/request-output-context.d.ts +1 -1
- package/dist/api/request-output-context.d.ts.map +1 -1
- package/dist/api/request-output-context.js.map +1 -1
- package/dist/api/route-caller.d.ts +30 -0
- package/dist/api/route-caller.d.ts.map +1 -0
- package/dist/api/route-caller.js +63 -0
- package/dist/api/route-caller.js.map +1 -0
- package/dist/api/route-handler-input-options.d.ts.map +1 -1
- package/dist/api/route.d.ts +8 -8
- package/dist/api/route.d.ts.map +1 -1
- package/dist/api/route.js.map +1 -1
- package/dist/api/shared-types.d.ts.map +1 -1
- package/dist/client/client-error.d.ts.map +1 -1
- package/dist/client/client-error.js.map +1 -1
- package/dist/client/client.d.ts +90 -50
- package/dist/client/client.d.ts.map +1 -1
- package/dist/client/client.js +128 -16
- package/dist/client/client.js.map +1 -1
- package/dist/client/client.svelte.d.ts +6 -5
- package/dist/client/client.svelte.d.ts.map +1 -1
- package/dist/client/client.svelte.js +10 -2
- package/dist/client/client.svelte.js.map +1 -1
- package/dist/client/internal/ndjson-streaming.js.map +1 -1
- package/dist/client/react.d.ts +5 -4
- package/dist/client/react.d.ts.map +1 -1
- package/dist/client/react.js +104 -12
- package/dist/client/react.js.map +1 -1
- package/dist/client/solid.d.ts +7 -5
- package/dist/client/solid.d.ts.map +1 -1
- package/dist/client/solid.js +23 -9
- package/dist/client/solid.js.map +1 -1
- package/dist/client/vanilla.d.ts +16 -4
- package/dist/client/vanilla.d.ts.map +1 -1
- package/dist/client/vanilla.js +21 -1
- package/dist/client/vanilla.js.map +1 -1
- package/dist/client/vue.d.ts +10 -4
- package/dist/client/vue.d.ts.map +1 -1
- package/dist/client/vue.js +24 -1
- package/dist/client/vue.js.map +1 -1
- package/dist/id.d.ts +2 -0
- package/dist/id.js +3 -0
- package/dist/internal/cuid.d.ts +16 -0
- package/dist/internal/cuid.d.ts.map +1 -0
- package/dist/internal/cuid.js +82 -0
- package/dist/internal/cuid.js.map +1 -0
- package/dist/internal/trace-context.d.ts +23 -0
- package/dist/internal/trace-context.d.ts.map +1 -0
- package/dist/internal/trace-context.js +14 -0
- package/dist/internal/trace-context.js.map +1 -0
- package/dist/mod-client.d.ts +7 -20
- package/dist/mod-client.d.ts.map +1 -1
- package/dist/mod-client.js +25 -13
- package/dist/mod-client.js.map +1 -1
- package/dist/mod.d.ts +8 -6
- package/dist/mod.js +3 -1
- package/dist/runtime.d.ts +15 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +33 -0
- package/dist/runtime.js.map +1 -0
- package/dist/test/test.d.ts +6 -6
- package/dist/test/test.d.ts.map +1 -1
- package/dist/test/test.js.map +1 -1
- package/dist/util/ssr.js.map +1 -1
- package/package.json +42 -52
- package/src/api/api.test.ts +3 -1
- package/src/api/api.ts +28 -0
- package/src/api/bind-services.ts +0 -5
- package/src/api/error.ts +1 -0
- package/src/api/fragment-definition-builder.extend.test.ts +2 -1
- package/src/api/fragment-definition-builder.test.ts +2 -1
- package/src/api/fragment-definition-builder.ts +56 -112
- package/src/api/fragment-instantiator.test.ts +311 -166
- package/src/api/fragment-instantiator.ts +470 -131
- package/src/api/fragment-services.test.ts +1 -0
- package/src/api/internal/path-runtime.test.ts +8 -0
- package/src/api/internal/path-type.test.ts +3 -1
- package/src/api/internal/route.test.ts +1 -0
- package/src/api/request-context-storage.ts +7 -0
- package/src/api/request-input-context.test.ts +156 -2
- package/src/api/request-input-context.ts +87 -1
- package/src/api/request-middleware.test.ts +43 -2
- package/src/api/request-middleware.ts +4 -3
- package/src/api/request-output-context.test.ts +3 -1
- package/src/api/request-output-context.ts +2 -1
- package/src/api/route-caller.test.ts +195 -0
- package/src/api/route-caller.ts +167 -0
- package/src/api/route-handler-input-options.ts +2 -1
- package/src/api/route.test.ts +4 -2
- package/src/api/route.ts +9 -3
- package/src/api/shared-types.ts +2 -1
- package/src/client/client-builder.test.ts +4 -2
- package/src/client/client-error.test.ts +2 -1
- package/src/client/client-error.ts +1 -1
- package/src/client/client-types.test.ts +19 -5
- package/src/client/client.ssr.test.ts +6 -4
- package/src/client/client.svelte.test.ts +18 -9
- package/src/client/client.svelte.ts +38 -13
- package/src/client/client.test.ts +244 -10
- package/src/client/client.ts +473 -148
- package/src/client/internal/ndjson-streaming.test.ts +6 -3
- package/src/client/internal/ndjson-streaming.ts +1 -0
- package/src/client/react.test.ts +176 -6
- package/src/client/react.ts +226 -31
- package/src/client/solid.test.ts +29 -5
- package/src/client/solid.ts +60 -22
- package/src/client/vanilla.test.ts +148 -6
- package/src/client/vanilla.ts +63 -9
- package/src/client/vue.test.ts +397 -8
- package/src/client/vue.ts +74 -4
- package/src/id.ts +1 -0
- package/src/internal/cuid.test.ts +164 -0
- package/src/internal/cuid.ts +133 -0
- package/src/internal/trace-context.ts +35 -0
- package/src/mod-client.ts +55 -9
- package/src/mod.ts +9 -3
- package/src/runtime.ts +48 -0
- package/src/test/test.test.ts +4 -2
- package/src/test/test.ts +14 -7
- package/src/util/async.test.ts +1 -0
- package/src/util/content-type.test.ts +1 -0
- package/src/util/nanostores.test.ts +3 -1
- package/src/util/ssr.ts +1 -0
- package/tsconfig.json +1 -1
- package/tsdown.config.ts +2 -0
- package/vitest.config.ts +2 -1
|
@@ -1,10 +1,26 @@
|
|
|
1
|
+
import { addRoute, createRouter, findRoute } from "rou3";
|
|
2
|
+
|
|
1
3
|
import type { StandardSchemaV1 } from "@standard-schema/spec";
|
|
4
|
+
|
|
5
|
+
import type { ExtractRouteByPath, ExtractRoutePath } from "../client/client";
|
|
6
|
+
import { instantiatedFragmentFakeSymbol } from "../internal/symbols";
|
|
7
|
+
import { recordTraceEvent } from "../internal/trace-context";
|
|
8
|
+
import type { InferOrUnknown } from "../util/types-util";
|
|
2
9
|
import { type FragnoRouteConfig, type HTTPMethod, type RequestThisContext } from "./api";
|
|
10
|
+
import { bindServicesToContext, type BoundServices } from "./bind-services";
|
|
3
11
|
import { FragnoApiError } from "./error";
|
|
12
|
+
import type { FragmentDefinition } from "./fragment-definition-builder";
|
|
13
|
+
import { type FragnoResponse, parseFragnoResponse } from "./fragno-response";
|
|
14
|
+
import type { ExtractPathParams } from "./internal/path";
|
|
4
15
|
import { getMountRoute } from "./internal/route";
|
|
5
|
-
import {
|
|
16
|
+
import { MutableRequestState } from "./mutable-request-state";
|
|
17
|
+
import { RequestContextStorage } from "./request-context-storage";
|
|
6
18
|
import { RequestInputContext, type RequestBodyType } from "./request-input-context";
|
|
7
|
-
import
|
|
19
|
+
import {
|
|
20
|
+
RequestMiddlewareInputContext,
|
|
21
|
+
RequestMiddlewareOutputContext,
|
|
22
|
+
type FragnoMiddlewareCallback,
|
|
23
|
+
} from "./request-middleware";
|
|
8
24
|
import { RequestOutputContext } from "./request-output-context";
|
|
9
25
|
import {
|
|
10
26
|
type AnyFragnoRouteConfig,
|
|
@@ -12,25 +28,98 @@ import {
|
|
|
12
28
|
type FlattenRouteFactories,
|
|
13
29
|
resolveRouteFactories,
|
|
14
30
|
} from "./route";
|
|
15
|
-
import {
|
|
16
|
-
RequestMiddlewareInputContext,
|
|
17
|
-
RequestMiddlewareOutputContext,
|
|
18
|
-
type FragnoMiddlewareCallback,
|
|
19
|
-
} from "./request-middleware";
|
|
20
|
-
import { MutableRequestState } from "./mutable-request-state";
|
|
21
31
|
import type { RouteHandlerInputOptions } from "./route-handler-input-options";
|
|
22
|
-
import type { ExtractRouteByPath, ExtractRoutePath } from "../client/client";
|
|
23
|
-
import { type FragnoResponse, parseFragnoResponse } from "./fragno-response";
|
|
24
|
-
import type { InferOrUnknown } from "../util/types-util";
|
|
25
|
-
import type { FragmentDefinition } from "./fragment-definition-builder";
|
|
26
32
|
import type { FragnoPublicConfig } from "./shared-types";
|
|
27
|
-
import { RequestContextStorage } from "./request-context-storage";
|
|
28
|
-
import { bindServicesToContext, type BoundServices } from "./bind-services";
|
|
29
|
-
import { instantiatedFragmentFakeSymbol } from "../internal/symbols";
|
|
30
33
|
|
|
31
34
|
// Re-export types needed by consumers
|
|
32
35
|
export type { BoundServices };
|
|
33
36
|
|
|
37
|
+
type CallRoutePath<TRoutes extends readonly AnyFragnoRouteConfig[], TMethod extends HTTPMethod> = [
|
|
38
|
+
ExtractRoutePath<TRoutes, TMethod>,
|
|
39
|
+
] extends [never]
|
|
40
|
+
? string
|
|
41
|
+
: ExtractRoutePath<TRoutes, TMethod>;
|
|
42
|
+
|
|
43
|
+
type CallRouteMatch<
|
|
44
|
+
TRoutes extends readonly AnyFragnoRouteConfig[],
|
|
45
|
+
TMethod extends HTTPMethod,
|
|
46
|
+
TPath extends string,
|
|
47
|
+
> = [ExtractRouteByPath<TRoutes, TPath, TMethod>] extends [never]
|
|
48
|
+
? AnyFragnoRouteConfig
|
|
49
|
+
: ExtractRouteByPath<TRoutes, TPath, TMethod>;
|
|
50
|
+
|
|
51
|
+
const requestSourceSymbol = Symbol.for("fragno-request-source");
|
|
52
|
+
const requestRouteSymbol = Symbol.for("fragno-request-route");
|
|
53
|
+
const requestWaitUntilSymbol = Symbol.for("fragno-request-wait-until");
|
|
54
|
+
|
|
55
|
+
type RequestRouteInfo = {
|
|
56
|
+
method: HTTPMethod;
|
|
57
|
+
path: string;
|
|
58
|
+
mountRoute?: string;
|
|
59
|
+
fullPath?: string;
|
|
60
|
+
};
|
|
61
|
+
type RequestSource = "route" | "context";
|
|
62
|
+
|
|
63
|
+
export type FragnoRequestLifecycleContext = {
|
|
64
|
+
waitUntil?: (promise: Promise<unknown>) => void;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
type InternalRoutePrefix = "/_internal";
|
|
68
|
+
|
|
69
|
+
type JoinInternalRoutePath<TPath extends string> = TPath extends "" | "/"
|
|
70
|
+
? InternalRoutePrefix
|
|
71
|
+
: TPath extends `/${string}`
|
|
72
|
+
? `${InternalRoutePrefix}${TPath}`
|
|
73
|
+
: `${InternalRoutePrefix}/${TPath}`;
|
|
74
|
+
|
|
75
|
+
type PrefixInternalRoute<TRoute> =
|
|
76
|
+
TRoute extends FragnoRouteConfig<
|
|
77
|
+
infer TMethod,
|
|
78
|
+
infer TPath,
|
|
79
|
+
infer TInputSchema,
|
|
80
|
+
infer TOutputSchema,
|
|
81
|
+
infer TErrorCode,
|
|
82
|
+
infer TQueryParameters,
|
|
83
|
+
infer TThisContext
|
|
84
|
+
>
|
|
85
|
+
? FragnoRouteConfig<
|
|
86
|
+
TMethod,
|
|
87
|
+
JoinInternalRoutePath<TPath>,
|
|
88
|
+
TInputSchema,
|
|
89
|
+
TOutputSchema,
|
|
90
|
+
TErrorCode,
|
|
91
|
+
TQueryParameters,
|
|
92
|
+
TThisContext
|
|
93
|
+
>
|
|
94
|
+
: never;
|
|
95
|
+
|
|
96
|
+
type PrefixInternalRoutes<TRoutes extends readonly AnyFragnoRouteConfig[]> =
|
|
97
|
+
TRoutes extends readonly [...infer TRoutesTuple]
|
|
98
|
+
? { [K in keyof TRoutesTuple]: PrefixInternalRoute<TRoutesTuple[K]> }
|
|
99
|
+
: readonly AnyFragnoRouteConfig[];
|
|
100
|
+
|
|
101
|
+
type InternalRoutesFromDefinition<TInternalRoutes extends readonly AnyRouteOrFactory[]> =
|
|
102
|
+
PrefixInternalRoutes<FlattenRouteFactories<TInternalRoutes>>;
|
|
103
|
+
|
|
104
|
+
export type RoutesWithInternal<
|
|
105
|
+
TRoutes extends readonly AnyFragnoRouteConfig[],
|
|
106
|
+
TInternalRoutes extends readonly AnyRouteOrFactory[],
|
|
107
|
+
> = readonly [...TRoutes, ...InternalRoutesFromDefinition<TInternalRoutes>];
|
|
108
|
+
|
|
109
|
+
type ExtractServiceCallResult<T> = T extends undefined
|
|
110
|
+
? undefined
|
|
111
|
+
: T extends { _internal: { finalResult?: infer R } }
|
|
112
|
+
? R
|
|
113
|
+
: Awaited<T>;
|
|
114
|
+
|
|
115
|
+
type ExtractServiceCallResults<T extends readonly unknown[]> = {
|
|
116
|
+
[K in keyof T]: ExtractServiceCallResult<T[K]>;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
type ExtractServiceCallResultsOrSingle<T> = T extends readonly unknown[]
|
|
120
|
+
? ExtractServiceCallResults<T>
|
|
121
|
+
: ExtractServiceCallResult<T>;
|
|
122
|
+
|
|
34
123
|
/**
|
|
35
124
|
* Helper type to extract the instantiated fragment type from a fragment definition.
|
|
36
125
|
* This is useful for typing functions that accept instantiated fragments based on their definition.
|
|
@@ -56,17 +145,16 @@ export type InstantiatedFragmentFromDefinition<
|
|
|
56
145
|
infer TServiceThisContext,
|
|
57
146
|
infer THandlerThisContext,
|
|
58
147
|
infer TRequestStorage,
|
|
59
|
-
infer
|
|
148
|
+
infer TInternalRoutes
|
|
60
149
|
>
|
|
61
150
|
? FragnoInstantiatedFragment<
|
|
62
|
-
readonly AnyFragnoRouteConfig[],
|
|
151
|
+
RoutesWithInternal<readonly AnyFragnoRouteConfig[], TInternalRoutes>,
|
|
63
152
|
TDeps,
|
|
64
153
|
BoundServices<TBaseServices & TServices>,
|
|
65
154
|
TServiceThisContext,
|
|
66
155
|
THandlerThisContext,
|
|
67
156
|
TRequestStorage,
|
|
68
|
-
TOptions
|
|
69
|
-
TLinkedFragments
|
|
157
|
+
TOptions
|
|
70
158
|
>
|
|
71
159
|
: never;
|
|
72
160
|
|
|
@@ -79,6 +167,34 @@ type ReactRouterHandlers = {
|
|
|
79
167
|
action: (args: { request: Request }) => Promise<Response>;
|
|
80
168
|
};
|
|
81
169
|
|
|
170
|
+
const serializeHeadersForTrace = (headers: Headers): [string, string][] =>
|
|
171
|
+
Array.from(headers.entries()).sort(([a], [b]) => a.localeCompare(b));
|
|
172
|
+
|
|
173
|
+
const serializeQueryForTrace = (query: URLSearchParams): [string, string][] =>
|
|
174
|
+
Array.from(query.entries()).sort(([a], [b]) => a.localeCompare(b));
|
|
175
|
+
|
|
176
|
+
const serializeBodyForTrace = (body: RequestBodyType): unknown => {
|
|
177
|
+
if (body instanceof FormData) {
|
|
178
|
+
const entries = Array.from(body.entries()).map(([key, value]) => {
|
|
179
|
+
if (value instanceof Blob) {
|
|
180
|
+
return [key, { type: "blob", size: value.size, mime: value.type }] as const;
|
|
181
|
+
}
|
|
182
|
+
return [key, value] as const;
|
|
183
|
+
});
|
|
184
|
+
return { type: "form-data", entries };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (body instanceof Blob) {
|
|
188
|
+
return { type: "blob", size: body.size, mime: body.type };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (body instanceof ReadableStream) {
|
|
192
|
+
return { type: "stream" };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return body;
|
|
196
|
+
};
|
|
197
|
+
|
|
82
198
|
type SolidStartHandlers = {
|
|
83
199
|
GET: (args: { request: Request }) => Promise<Response>;
|
|
84
200
|
POST: (args: { request: Request }) => Promise<Response>;
|
|
@@ -126,11 +242,27 @@ export type AnyFragnoInstantiatedFragment = FragnoInstantiatedFragment<
|
|
|
126
242
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
127
243
|
any,
|
|
128
244
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
129
|
-
any,
|
|
130
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
131
245
|
any
|
|
132
246
|
>;
|
|
133
247
|
|
|
248
|
+
const INTERNAL_ROUTE_PREFIX = "/_internal";
|
|
249
|
+
|
|
250
|
+
function normalizeRoutePrefix(prefix: string): string {
|
|
251
|
+
if (!prefix.startsWith("/")) {
|
|
252
|
+
prefix = `/${prefix}`;
|
|
253
|
+
}
|
|
254
|
+
return prefix.endsWith("/") && prefix.length > 1 ? prefix.slice(0, -1) : prefix;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function joinRoutePath(prefix: string, path: string): string {
|
|
258
|
+
const normalizedPrefix = normalizeRoutePrefix(prefix);
|
|
259
|
+
if (!path || path === "/") {
|
|
260
|
+
return normalizedPrefix;
|
|
261
|
+
}
|
|
262
|
+
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
|
|
263
|
+
return `${normalizedPrefix}${normalizedPath}`;
|
|
264
|
+
}
|
|
265
|
+
|
|
134
266
|
export interface FragnoFragmentSharedConfig<
|
|
135
267
|
TRoutes extends readonly FragnoRouteConfig<
|
|
136
268
|
HTTPMethod,
|
|
@@ -157,9 +289,7 @@ export class FragnoInstantiatedFragment<
|
|
|
157
289
|
THandlerThisContext extends RequestThisContext,
|
|
158
290
|
TRequestStorage = {},
|
|
159
291
|
TOptions extends FragnoPublicConfig = FragnoPublicConfig,
|
|
160
|
-
|
|
161
|
-
> implements IFragnoInstantiatedFragment
|
|
162
|
-
{
|
|
292
|
+
> implements IFragnoInstantiatedFragment {
|
|
163
293
|
readonly [instantiatedFragmentFakeSymbol] = instantiatedFragmentFakeSymbol;
|
|
164
294
|
|
|
165
295
|
// Private fields
|
|
@@ -175,7 +305,7 @@ export class FragnoInstantiatedFragment<
|
|
|
175
305
|
#contextStorage: RequestContextStorage<TRequestStorage>;
|
|
176
306
|
#createRequestStorage?: () => TRequestStorage;
|
|
177
307
|
#options: TOptions;
|
|
178
|
-
#
|
|
308
|
+
#internalData: Record<string, unknown>;
|
|
179
309
|
|
|
180
310
|
constructor(params: {
|
|
181
311
|
name: string;
|
|
@@ -188,7 +318,7 @@ export class FragnoInstantiatedFragment<
|
|
|
188
318
|
storage: RequestContextStorage<TRequestStorage>;
|
|
189
319
|
createRequestStorage?: () => TRequestStorage;
|
|
190
320
|
options: TOptions;
|
|
191
|
-
|
|
321
|
+
internalData?: Record<string, unknown>;
|
|
192
322
|
}) {
|
|
193
323
|
this.#name = params.name;
|
|
194
324
|
this.#routes = params.routes;
|
|
@@ -200,7 +330,7 @@ export class FragnoInstantiatedFragment<
|
|
|
200
330
|
this.#contextStorage = params.storage;
|
|
201
331
|
this.#createRequestStorage = params.createRequestStorage;
|
|
202
332
|
this.#options = params.options;
|
|
203
|
-
this.#
|
|
333
|
+
this.#internalData = params.internalData ?? {};
|
|
204
334
|
|
|
205
335
|
// Build router
|
|
206
336
|
this.#router =
|
|
@@ -248,7 +378,7 @@ export class FragnoInstantiatedFragment<
|
|
|
248
378
|
return {
|
|
249
379
|
deps: this.#deps,
|
|
250
380
|
options: this.#options,
|
|
251
|
-
|
|
381
|
+
...this.#internalData,
|
|
252
382
|
};
|
|
253
383
|
}
|
|
254
384
|
|
|
@@ -269,9 +399,24 @@ export class FragnoInstantiatedFragment<
|
|
|
269
399
|
* This is a shared helper used by inContext(), handler(), and callRouteRaw().
|
|
270
400
|
* @private
|
|
271
401
|
*/
|
|
272
|
-
#withRequestStorage<T>(
|
|
273
|
-
|
|
274
|
-
|
|
402
|
+
#withRequestStorage<T>(
|
|
403
|
+
callback: () => T,
|
|
404
|
+
source?: RequestSource,
|
|
405
|
+
routeInfo?: RequestRouteInfo,
|
|
406
|
+
lifecycleContext?: FragnoRequestLifecycleContext,
|
|
407
|
+
): T;
|
|
408
|
+
#withRequestStorage<T>(
|
|
409
|
+
callback: () => Promise<T>,
|
|
410
|
+
source?: RequestSource,
|
|
411
|
+
routeInfo?: RequestRouteInfo,
|
|
412
|
+
lifecycleContext?: FragnoRequestLifecycleContext,
|
|
413
|
+
): Promise<T>;
|
|
414
|
+
#withRequestStorage<T>(
|
|
415
|
+
callback: () => T | Promise<T>,
|
|
416
|
+
source: RequestSource = "context",
|
|
417
|
+
routeInfo?: RequestRouteInfo,
|
|
418
|
+
lifecycleContext?: FragnoRequestLifecycleContext,
|
|
419
|
+
): T | Promise<T> {
|
|
275
420
|
if (!this.#serviceThisContext && !this.#handlerThisContext) {
|
|
276
421
|
// No request context configured - just run callback directly
|
|
277
422
|
return callback();
|
|
@@ -281,6 +426,16 @@ export class FragnoInstantiatedFragment<
|
|
|
281
426
|
const storageData = this.#createRequestStorage
|
|
282
427
|
? this.#createRequestStorage()
|
|
283
428
|
: ({} as TRequestStorage);
|
|
429
|
+
if (storageData && typeof storageData === "object") {
|
|
430
|
+
const metadataTarget = storageData as Record<symbol, unknown>;
|
|
431
|
+
metadataTarget[requestSourceSymbol] = source;
|
|
432
|
+
if (routeInfo) {
|
|
433
|
+
metadataTarget[requestRouteSymbol] = routeInfo;
|
|
434
|
+
}
|
|
435
|
+
if (lifecycleContext?.waitUntil) {
|
|
436
|
+
metadataTarget[requestWaitUntilSymbol] = lifecycleContext.waitUntil;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
284
439
|
return this.#contextStorage.run(storageData, callback);
|
|
285
440
|
}
|
|
286
441
|
|
|
@@ -308,9 +463,55 @@ export class FragnoInstantiatedFragment<
|
|
|
308
463
|
// Always use handler context for inContext - it has full capabilities
|
|
309
464
|
if (this.#handlerThisContext) {
|
|
310
465
|
const boundCallback = callback.bind(this.#handlerThisContext);
|
|
311
|
-
return this.#withRequestStorage(boundCallback);
|
|
466
|
+
return this.#withRequestStorage(boundCallback, "context");
|
|
467
|
+
}
|
|
468
|
+
return this.#withRequestStorage(callback, "context");
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Execute multiple service calls within a handler context.
|
|
473
|
+
* If called outside a request context, it will create one automatically.
|
|
474
|
+
* Pass a factory so service calls are created inside the active context.
|
|
475
|
+
* Primarily used by database fragments (handlerTx).
|
|
476
|
+
*/
|
|
477
|
+
async callServices<TServiceCalls>(
|
|
478
|
+
serviceCalls: () => TServiceCalls,
|
|
479
|
+
): Promise<ExtractServiceCallResultsOrSingle<TServiceCalls>> {
|
|
480
|
+
const handlerContext = this.#handlerThisContext as
|
|
481
|
+
| {
|
|
482
|
+
handlerTx?: () => {
|
|
483
|
+
withServiceCalls: (fn: () => readonly unknown[]) => { execute: () => Promise<unknown> };
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
| undefined;
|
|
487
|
+
|
|
488
|
+
if (!handlerContext?.handlerTx) {
|
|
489
|
+
throw new Error(
|
|
490
|
+
"callServices is only supported for fragments with handlerTx (database fragments).",
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
let callWasArray = false;
|
|
495
|
+
const execute = () => {
|
|
496
|
+
return handlerContext.handlerTx!()
|
|
497
|
+
.withServiceCalls(() => {
|
|
498
|
+
const calls = serviceCalls();
|
|
499
|
+
callWasArray = Array.isArray(calls);
|
|
500
|
+
return (callWasArray ? calls : [calls]) as readonly unknown[];
|
|
501
|
+
})
|
|
502
|
+
.execute();
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
const result = this.#contextStorage.hasStore()
|
|
506
|
+
? await execute()
|
|
507
|
+
: await this.#withRequestStorage(execute, "context");
|
|
508
|
+
|
|
509
|
+
if (callWasArray) {
|
|
510
|
+
return result as ExtractServiceCallResultsOrSingle<TServiceCalls>;
|
|
312
511
|
}
|
|
313
|
-
|
|
512
|
+
|
|
513
|
+
const [first] = result as unknown[];
|
|
514
|
+
return first as ExtractServiceCallResultsOrSingle<TServiceCalls>;
|
|
314
515
|
}
|
|
315
516
|
|
|
316
517
|
/**
|
|
@@ -379,7 +580,7 @@ export class FragnoInstantiatedFragment<
|
|
|
379
580
|
* Main request handler for this fragment.
|
|
380
581
|
* Handles routing, middleware, and error handling.
|
|
381
582
|
*/
|
|
382
|
-
async handler(req: Request): Promise<Response> {
|
|
583
|
+
async handler(req: Request, lifecycleContext?: FragnoRequestLifecycleContext): Promise<Response> {
|
|
383
584
|
const url = new URL(req.url);
|
|
384
585
|
const pathname = url.pathname;
|
|
385
586
|
|
|
@@ -409,44 +610,112 @@ export class FragnoInstantiatedFragment<
|
|
|
409
610
|
);
|
|
410
611
|
}
|
|
411
612
|
|
|
412
|
-
//
|
|
613
|
+
// Get the expected content type from route config (default: application/json)
|
|
614
|
+
const routeConfig = route.data as AnyFragnoRouteConfig;
|
|
615
|
+
const expectedContentType = routeConfig.contentType ?? "application/json";
|
|
616
|
+
|
|
617
|
+
// Parse request body based on route's expected content type
|
|
413
618
|
let requestBody: RequestBodyType = undefined;
|
|
414
619
|
let rawBody: string | undefined = undefined;
|
|
415
620
|
|
|
416
621
|
if (req.body instanceof ReadableStream) {
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
622
|
+
const requestContentType = (req.headers.get("content-type") ?? "").toLowerCase();
|
|
623
|
+
|
|
624
|
+
if (expectedContentType === "multipart/form-data") {
|
|
625
|
+
// Route expects FormData (file uploads)
|
|
626
|
+
if (!requestContentType.includes("multipart/form-data")) {
|
|
627
|
+
return Response.json(
|
|
628
|
+
{
|
|
629
|
+
error: `This endpoint expects multipart/form-data, but received: ${requestContentType || "no content-type"}`,
|
|
630
|
+
code: "UNSUPPORTED_MEDIA_TYPE",
|
|
631
|
+
},
|
|
632
|
+
{ status: 415 },
|
|
633
|
+
);
|
|
634
|
+
}
|
|
422
635
|
|
|
423
|
-
// Parse JSON if body is not empty
|
|
424
|
-
if (rawBody) {
|
|
425
636
|
try {
|
|
426
|
-
requestBody =
|
|
637
|
+
requestBody = await req.formData();
|
|
427
638
|
} catch {
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
639
|
+
return Response.json(
|
|
640
|
+
{ error: "Failed to parse multipart form data", code: "INVALID_REQUEST_BODY" },
|
|
641
|
+
{ status: 400 },
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
} else if (expectedContentType === "application/octet-stream") {
|
|
645
|
+
if (!requestContentType.includes("application/octet-stream")) {
|
|
646
|
+
return Response.json(
|
|
647
|
+
{
|
|
648
|
+
error: `This endpoint expects application/octet-stream, but received: ${requestContentType || "no content-type"}`,
|
|
649
|
+
code: "UNSUPPORTED_MEDIA_TYPE",
|
|
650
|
+
},
|
|
651
|
+
{ status: 415 },
|
|
652
|
+
);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
requestBody = req.body ?? new ReadableStream<Uint8Array>();
|
|
656
|
+
} else {
|
|
657
|
+
// Route expects JSON (default)
|
|
658
|
+
// Note: We're lenient here - we accept requests without Content-Type header
|
|
659
|
+
// or with application/json. We reject multipart/form-data for JSON routes.
|
|
660
|
+
if (requestContentType.includes("multipart/form-data")) {
|
|
661
|
+
return Response.json(
|
|
662
|
+
{
|
|
663
|
+
error: `This endpoint expects JSON, but received multipart/form-data. Use a route with contentType: "multipart/form-data" for file uploads.`,
|
|
664
|
+
code: "UNSUPPORTED_MEDIA_TYPE",
|
|
665
|
+
},
|
|
666
|
+
{ status: 415 },
|
|
667
|
+
);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// Clone request to make sure we don't consume body stream
|
|
671
|
+
const clonedReq = req.clone();
|
|
672
|
+
|
|
673
|
+
// Get raw text
|
|
674
|
+
rawBody = await clonedReq.text();
|
|
675
|
+
|
|
676
|
+
// Parse JSON if body is not empty
|
|
677
|
+
if (rawBody) {
|
|
678
|
+
try {
|
|
679
|
+
requestBody = JSON.parse(rawBody);
|
|
680
|
+
} catch {
|
|
681
|
+
// If JSON parsing fails, keep body as undefined
|
|
682
|
+
// This handles cases where body is not JSON
|
|
683
|
+
requestBody = undefined;
|
|
684
|
+
}
|
|
431
685
|
}
|
|
432
686
|
}
|
|
433
687
|
}
|
|
434
688
|
|
|
689
|
+
// URL decode path params from rou3 (which returns encoded values)
|
|
690
|
+
const decodedRouteParams: Record<string, string> = {};
|
|
691
|
+
for (const [key, value] of Object.entries(route.params ?? {})) {
|
|
692
|
+
decodedRouteParams[key] = decodeURIComponent(value);
|
|
693
|
+
}
|
|
694
|
+
|
|
435
695
|
const requestState = new MutableRequestState({
|
|
436
|
-
pathParams:
|
|
696
|
+
pathParams: decodedRouteParams,
|
|
437
697
|
searchParams: url.searchParams,
|
|
438
698
|
body: requestBody,
|
|
439
699
|
headers: new Headers(req.headers),
|
|
440
700
|
});
|
|
441
701
|
|
|
702
|
+
const fullRoutePath =
|
|
703
|
+
this.#mountRoute && this.#mountRoute !== "/"
|
|
704
|
+
? `${this.#mountRoute}${routeConfig.path}`
|
|
705
|
+
: routeConfig.path;
|
|
706
|
+
const routeInfo: RequestRouteInfo = {
|
|
707
|
+
method: routeConfig.method,
|
|
708
|
+
path: routeConfig.path,
|
|
709
|
+
mountRoute: this.#mountRoute,
|
|
710
|
+
fullPath: fullRoutePath,
|
|
711
|
+
};
|
|
712
|
+
|
|
442
713
|
// Execute middleware and handler
|
|
443
714
|
const executeRequest = async (): Promise<Response> => {
|
|
444
|
-
//
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
return middlewareResult;
|
|
449
|
-
}
|
|
715
|
+
// Parent middleware execution
|
|
716
|
+
const middlewareResult = await this.#executeMiddleware(req, route, requestState);
|
|
717
|
+
if (middlewareResult !== undefined) {
|
|
718
|
+
return middlewareResult;
|
|
450
719
|
}
|
|
451
720
|
|
|
452
721
|
// Handler execution
|
|
@@ -454,23 +723,23 @@ export class FragnoInstantiatedFragment<
|
|
|
454
723
|
};
|
|
455
724
|
|
|
456
725
|
// Wrap with request storage context if provided
|
|
457
|
-
return this.#withRequestStorage(executeRequest);
|
|
726
|
+
return this.#withRequestStorage(executeRequest, "route", routeInfo, lifecycleContext);
|
|
458
727
|
}
|
|
459
728
|
|
|
460
729
|
/**
|
|
461
730
|
* Call a route directly with typed inputs and outputs.
|
|
462
731
|
* Useful for testing and server-side route calls.
|
|
463
732
|
*/
|
|
464
|
-
async callRoute<TMethod extends HTTPMethod, TPath extends
|
|
733
|
+
async callRoute<TMethod extends HTTPMethod, TPath extends CallRoutePath<TRoutes, TMethod>>(
|
|
465
734
|
method: TMethod,
|
|
466
735
|
path: TPath,
|
|
467
736
|
inputOptions?: RouteHandlerInputOptions<
|
|
468
737
|
TPath,
|
|
469
|
-
|
|
738
|
+
CallRouteMatch<TRoutes, TMethod, TPath>["inputSchema"]
|
|
470
739
|
>,
|
|
471
740
|
): Promise<
|
|
472
741
|
FragnoResponse<
|
|
473
|
-
InferOrUnknown<NonNullable<
|
|
742
|
+
InferOrUnknown<NonNullable<CallRouteMatch<TRoutes, TMethod, TPath>["outputSchema"]>>
|
|
474
743
|
>
|
|
475
744
|
> {
|
|
476
745
|
const response = await this.callRouteRaw(method, path, inputOptions);
|
|
@@ -481,12 +750,12 @@ export class FragnoInstantiatedFragment<
|
|
|
481
750
|
* Call a route directly and get the raw Response object.
|
|
482
751
|
* Useful for testing and server-side route calls.
|
|
483
752
|
*/
|
|
484
|
-
async callRouteRaw<TMethod extends HTTPMethod, TPath extends
|
|
753
|
+
async callRouteRaw<TMethod extends HTTPMethod, TPath extends CallRoutePath<TRoutes, TMethod>>(
|
|
485
754
|
method: TMethod,
|
|
486
755
|
path: TPath,
|
|
487
756
|
inputOptions?: RouteHandlerInputOptions<
|
|
488
757
|
TPath,
|
|
489
|
-
|
|
758
|
+
CallRouteMatch<TRoutes, TMethod, TPath>["inputSchema"]
|
|
490
759
|
>,
|
|
491
760
|
): Promise<Response> {
|
|
492
761
|
// Find route in this.#routes
|
|
@@ -502,7 +771,8 @@ export class FragnoInstantiatedFragment<
|
|
|
502
771
|
);
|
|
503
772
|
}
|
|
504
773
|
|
|
505
|
-
const { pathParams = {},
|
|
774
|
+
const { pathParams = {}, query, headers } = inputOptions || {};
|
|
775
|
+
const body = inputOptions && "body" in inputOptions ? inputOptions.body : undefined;
|
|
506
776
|
|
|
507
777
|
// Convert query to URLSearchParams if needed
|
|
508
778
|
const searchParams =
|
|
@@ -527,9 +797,30 @@ export class FragnoInstantiatedFragment<
|
|
|
527
797
|
shouldValidateInput: true, // Enable validation for production use
|
|
528
798
|
});
|
|
529
799
|
|
|
800
|
+
recordTraceEvent({
|
|
801
|
+
type: "route-input",
|
|
802
|
+
method: route.method,
|
|
803
|
+
path: route.path,
|
|
804
|
+
pathParams: (pathParams ?? {}) as Record<string, string>,
|
|
805
|
+
queryParams: serializeQueryForTrace(searchParams),
|
|
806
|
+
headers: serializeHeadersForTrace(requestHeaders),
|
|
807
|
+
body: serializeBodyForTrace(body),
|
|
808
|
+
});
|
|
809
|
+
|
|
530
810
|
// Construct RequestOutputContext
|
|
531
811
|
const outputContext = new RequestOutputContext(route.outputSchema);
|
|
532
812
|
|
|
813
|
+
const fullRoutePath =
|
|
814
|
+
this.#mountRoute && this.#mountRoute !== "/"
|
|
815
|
+
? `${this.#mountRoute}${route.path}`
|
|
816
|
+
: route.path;
|
|
817
|
+
const routeInfo: RequestRouteInfo = {
|
|
818
|
+
method: route.method,
|
|
819
|
+
path: route.path,
|
|
820
|
+
mountRoute: this.#mountRoute,
|
|
821
|
+
fullPath: fullRoutePath,
|
|
822
|
+
};
|
|
823
|
+
|
|
533
824
|
// Execute handler
|
|
534
825
|
const executeHandler = async (): Promise<Response> => {
|
|
535
826
|
try {
|
|
@@ -551,7 +842,7 @@ export class FragnoInstantiatedFragment<
|
|
|
551
842
|
};
|
|
552
843
|
|
|
553
844
|
// Wrap with request storage context if provided
|
|
554
|
-
return this.#withRequestStorage(executeHandler);
|
|
845
|
+
return this.#withRequestStorage(executeHandler, "route", routeInfo);
|
|
555
846
|
}
|
|
556
847
|
|
|
557
848
|
/**
|
|
@@ -563,32 +854,73 @@ export class FragnoInstantiatedFragment<
|
|
|
563
854
|
route: ReturnType<typeof findRoute>,
|
|
564
855
|
requestState: MutableRequestState,
|
|
565
856
|
): Promise<Response | undefined> {
|
|
566
|
-
if (!
|
|
857
|
+
if (!route) {
|
|
567
858
|
return undefined;
|
|
568
859
|
}
|
|
569
860
|
|
|
570
861
|
const { path } = route.data as AnyFragnoRouteConfig;
|
|
571
|
-
|
|
572
|
-
|
|
862
|
+
return FragnoInstantiatedFragment.#runMiddlewareForFragment(this, {
|
|
863
|
+
req,
|
|
573
864
|
method: req.method as HTTPMethod,
|
|
574
865
|
path,
|
|
575
|
-
|
|
576
|
-
state: requestState,
|
|
866
|
+
requestState,
|
|
577
867
|
});
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
static async #runMiddlewareForFragment(
|
|
871
|
+
fragment: AnyFragnoInstantiatedFragment,
|
|
872
|
+
options: {
|
|
873
|
+
req: Request;
|
|
874
|
+
method: HTTPMethod;
|
|
875
|
+
path: string;
|
|
876
|
+
requestState: MutableRequestState;
|
|
877
|
+
routes?: readonly AnyFragnoRouteConfig[];
|
|
878
|
+
},
|
|
879
|
+
): Promise<Response | undefined> {
|
|
880
|
+
if (!fragment.#middlewareHandler) {
|
|
881
|
+
return undefined;
|
|
882
|
+
}
|
|
578
883
|
|
|
579
|
-
const
|
|
884
|
+
const middlewareInputContext = new RequestMiddlewareInputContext(
|
|
885
|
+
(options.routes ?? fragment.#routes) as readonly AnyFragnoRouteConfig[],
|
|
886
|
+
{
|
|
887
|
+
method: options.method,
|
|
888
|
+
path: options.path,
|
|
889
|
+
request: options.req,
|
|
890
|
+
state: options.requestState,
|
|
891
|
+
},
|
|
892
|
+
);
|
|
893
|
+
|
|
894
|
+
const middlewareOutputContext = new RequestMiddlewareOutputContext(
|
|
895
|
+
fragment.#deps,
|
|
896
|
+
fragment.#services,
|
|
897
|
+
);
|
|
580
898
|
|
|
581
899
|
try {
|
|
582
|
-
const middlewareResult = await
|
|
900
|
+
const middlewareResult = await fragment.#middlewareHandler(
|
|
583
901
|
middlewareInputContext,
|
|
584
902
|
middlewareOutputContext,
|
|
585
903
|
);
|
|
904
|
+
recordTraceEvent({
|
|
905
|
+
type: "middleware-decision",
|
|
906
|
+
method: options.method,
|
|
907
|
+
path: options.path,
|
|
908
|
+
outcome: middlewareResult ? "deny" : "allow",
|
|
909
|
+
status: middlewareResult?.status,
|
|
910
|
+
});
|
|
586
911
|
if (middlewareResult !== undefined) {
|
|
587
912
|
return middlewareResult;
|
|
588
913
|
}
|
|
589
914
|
} catch (error) {
|
|
590
915
|
console.error("Error in middleware", error);
|
|
591
916
|
|
|
917
|
+
recordTraceEvent({
|
|
918
|
+
type: "middleware-decision",
|
|
919
|
+
method: options.method,
|
|
920
|
+
path: options.path,
|
|
921
|
+
outcome: "deny",
|
|
922
|
+
status: error instanceof FragnoApiError ? error.status : 500,
|
|
923
|
+
});
|
|
592
924
|
if (error instanceof FragnoApiError) {
|
|
593
925
|
return error.toResponse();
|
|
594
926
|
}
|
|
@@ -627,6 +959,16 @@ export class FragnoInstantiatedFragment<
|
|
|
627
959
|
rawBody,
|
|
628
960
|
});
|
|
629
961
|
|
|
962
|
+
recordTraceEvent({
|
|
963
|
+
type: "route-input",
|
|
964
|
+
method: req.method,
|
|
965
|
+
path,
|
|
966
|
+
pathParams: inputContext.pathParams as Record<string, string>,
|
|
967
|
+
queryParams: serializeQueryForTrace(requestState.searchParams),
|
|
968
|
+
headers: serializeHeadersForTrace(requestState.headers),
|
|
969
|
+
body: serializeBodyForTrace(requestState.body),
|
|
970
|
+
});
|
|
971
|
+
|
|
630
972
|
const outputContext = new RequestOutputContext(outputSchema);
|
|
631
973
|
|
|
632
974
|
try {
|
|
@@ -679,7 +1021,7 @@ export function instantiateFragment<
|
|
|
679
1021
|
const THandlerThisContext extends RequestThisContext,
|
|
680
1022
|
const TRequestStorage,
|
|
681
1023
|
const TRoutesOrFactories extends readonly AnyRouteOrFactory[],
|
|
682
|
-
const
|
|
1024
|
+
const TInternalRoutes extends readonly AnyRouteOrFactory[],
|
|
683
1025
|
>(
|
|
684
1026
|
definition: FragmentDefinition<
|
|
685
1027
|
TConfig,
|
|
@@ -692,7 +1034,7 @@ export function instantiateFragment<
|
|
|
692
1034
|
TServiceThisContext,
|
|
693
1035
|
THandlerThisContext,
|
|
694
1036
|
TRequestStorage,
|
|
695
|
-
|
|
1037
|
+
TInternalRoutes
|
|
696
1038
|
>,
|
|
697
1039
|
config: TConfig,
|
|
698
1040
|
routesOrFactories: TRoutesOrFactories,
|
|
@@ -700,14 +1042,13 @@ export function instantiateFragment<
|
|
|
700
1042
|
serviceImplementations?: TServiceDependencies,
|
|
701
1043
|
instantiationOptions?: InstantiationOptions,
|
|
702
1044
|
): FragnoInstantiatedFragment<
|
|
703
|
-
FlattenRouteFactories<TRoutesOrFactories>,
|
|
1045
|
+
RoutesWithInternal<FlattenRouteFactories<TRoutesOrFactories>, TInternalRoutes>,
|
|
704
1046
|
TDeps,
|
|
705
1047
|
BoundServices<TBaseServices & TServices>,
|
|
706
1048
|
TServiceThisContext,
|
|
707
1049
|
THandlerThisContext,
|
|
708
1050
|
TRequestStorage,
|
|
709
|
-
TOptions
|
|
710
|
-
TLinkedFragments
|
|
1051
|
+
TOptions
|
|
711
1052
|
> {
|
|
712
1053
|
const { dryRun = false } = instantiationOptions ?? {};
|
|
713
1054
|
|
|
@@ -742,36 +1083,18 @@ export function instantiateFragment<
|
|
|
742
1083
|
}
|
|
743
1084
|
}
|
|
744
1085
|
|
|
745
|
-
// 3.
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
if (definition.linkedFragments) {
|
|
751
|
-
for (const [name, callback] of Object.entries(definition.linkedFragments)) {
|
|
752
|
-
const linkedFragment = callback({
|
|
753
|
-
config,
|
|
754
|
-
options,
|
|
755
|
-
serviceDependencies: serviceImplementations,
|
|
756
|
-
});
|
|
757
|
-
(linkedFragmentInstances as Record<string, AnyFragnoInstantiatedFragment>)[name] =
|
|
758
|
-
linkedFragment;
|
|
759
|
-
|
|
760
|
-
// Merge all services from linked fragment into private services directly by their service name
|
|
761
|
-
const services = linkedFragment.services as Record<string, unknown>;
|
|
762
|
-
for (const [serviceName, service] of Object.entries(services)) {
|
|
763
|
-
linkedFragmentServices[serviceName] = service;
|
|
764
|
-
}
|
|
765
|
-
}
|
|
766
|
-
}
|
|
1086
|
+
// 3. Calculate mount route early so internal routes can reference it
|
|
1087
|
+
const mountRoute = getMountRoute({
|
|
1088
|
+
name: definition.name,
|
|
1089
|
+
mountRoute: options.mountRoute,
|
|
1090
|
+
});
|
|
767
1091
|
|
|
768
|
-
// Identity function for service definition (used to set 'this' context)
|
|
1092
|
+
// 4. Identity function for service definition (used to set 'this' context)
|
|
769
1093
|
const defineService = <T>(services: T & ThisType<TServiceThisContext>): T => services;
|
|
770
1094
|
|
|
771
|
-
//
|
|
1095
|
+
// 5. Call privateServices factories
|
|
772
1096
|
// Private services are instantiated in order, so earlier ones are available to later ones
|
|
773
|
-
|
|
774
|
-
const privateServices = { ...linkedFragmentServices } as TPrivateServices;
|
|
1097
|
+
const privateServices = {} as TPrivateServices;
|
|
775
1098
|
if (definition.privateServices) {
|
|
776
1099
|
for (const [serviceName, factory] of Object.entries(definition.privateServices)) {
|
|
777
1100
|
const serviceFactory = factory as (context: {
|
|
@@ -806,7 +1129,7 @@ export function instantiateFragment<
|
|
|
806
1129
|
}
|
|
807
1130
|
}
|
|
808
1131
|
|
|
809
|
-
//
|
|
1132
|
+
// 6. Call baseServices callback (with access to private services)
|
|
810
1133
|
let baseServices: TBaseServices;
|
|
811
1134
|
try {
|
|
812
1135
|
baseServices =
|
|
@@ -830,7 +1153,7 @@ export function instantiateFragment<
|
|
|
830
1153
|
}
|
|
831
1154
|
}
|
|
832
1155
|
|
|
833
|
-
//
|
|
1156
|
+
// 7. Call namedServices factories (with access to private services)
|
|
834
1157
|
const namedServices = {} as TServices;
|
|
835
1158
|
if (definition.namedServices) {
|
|
836
1159
|
for (const [serviceName, factory] of Object.entries(definition.namedServices)) {
|
|
@@ -866,13 +1189,13 @@ export function instantiateFragment<
|
|
|
866
1189
|
}
|
|
867
1190
|
}
|
|
868
1191
|
|
|
869
|
-
//
|
|
1192
|
+
// 8. Merge public services (NOT including private services)
|
|
870
1193
|
const services = {
|
|
871
1194
|
...baseServices,
|
|
872
1195
|
...namedServices,
|
|
873
1196
|
};
|
|
874
1197
|
|
|
875
|
-
//
|
|
1198
|
+
// 9. Create request context storage and both service & handler contexts
|
|
876
1199
|
// Use external storage if provided, otherwise create new storage
|
|
877
1200
|
const storage = definition.getExternalStorage
|
|
878
1201
|
? definition.getExternalStorage({ config, options, deps })
|
|
@@ -889,24 +1212,37 @@ export function instantiateFragment<
|
|
|
889
1212
|
const serviceContext = contexts?.serviceContext;
|
|
890
1213
|
const handlerContext = contexts?.handlerContext;
|
|
891
1214
|
|
|
892
|
-
//
|
|
1215
|
+
// 10. Bind services to serviceContext (restricted)
|
|
893
1216
|
// Services get the restricted context (for database fragments, this excludes execute methods)
|
|
894
1217
|
const boundServices = serviceContext ? bindServicesToContext(services, serviceContext) : services;
|
|
895
|
-
|
|
896
|
-
|
|
1218
|
+
const internalData =
|
|
1219
|
+
definition.internalDataFactory?.({
|
|
1220
|
+
config,
|
|
1221
|
+
options,
|
|
1222
|
+
deps,
|
|
1223
|
+
services: boundServices as BoundServices<TBaseServices & TServices>,
|
|
1224
|
+
serviceDeps: (serviceImplementations ?? {}) as TServiceDependencies,
|
|
1225
|
+
}) ?? {};
|
|
1226
|
+
|
|
1227
|
+
// 11. Resolve routes with bound services
|
|
897
1228
|
const context = {
|
|
898
1229
|
config,
|
|
899
1230
|
deps,
|
|
900
1231
|
services: boundServices,
|
|
901
1232
|
serviceDeps: serviceImplementations ?? ({} as TServiceDependencies),
|
|
902
1233
|
};
|
|
903
|
-
const routes = resolveRouteFactories(context, routesOrFactories);
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
1234
|
+
const routes = resolveRouteFactories(context, routesOrFactories) as AnyFragnoRouteConfig[];
|
|
1235
|
+
const internalRoutes = definition.internalRoutes
|
|
1236
|
+
? (resolveRouteFactories(context, definition.internalRoutes) as readonly AnyFragnoRouteConfig[])
|
|
1237
|
+
: [];
|
|
1238
|
+
const prefixedInternalRoutes = internalRoutes.map((route) => ({
|
|
1239
|
+
...route,
|
|
1240
|
+
path: joinRoutePath(INTERNAL_ROUTE_PREFIX, route.path),
|
|
1241
|
+
}));
|
|
1242
|
+
const finalRoutes =
|
|
1243
|
+
prefixedInternalRoutes.length > 0
|
|
1244
|
+
? [...routes, ...prefixedInternalRoutes]
|
|
1245
|
+
: (routes as AnyFragnoRouteConfig[]);
|
|
910
1246
|
|
|
911
1247
|
// 12. Wrap createRequestStorage to capture context
|
|
912
1248
|
const createRequestStorageWithContext = definition.createRequestStorage
|
|
@@ -918,7 +1254,10 @@ export function instantiateFragment<
|
|
|
918
1254
|
// Handlers get handlerContext which may have more capabilities than serviceContext
|
|
919
1255
|
return new FragnoInstantiatedFragment({
|
|
920
1256
|
name: definition.name,
|
|
921
|
-
routes
|
|
1257
|
+
routes: finalRoutes as unknown as RoutesWithInternal<
|
|
1258
|
+
FlattenRouteFactories<TRoutesOrFactories>,
|
|
1259
|
+
TInternalRoutes
|
|
1260
|
+
>,
|
|
922
1261
|
deps,
|
|
923
1262
|
services: boundServices as BoundServices<TBaseServices & TServices>,
|
|
924
1263
|
mountRoute,
|
|
@@ -927,7 +1266,7 @@ export function instantiateFragment<
|
|
|
927
1266
|
storage,
|
|
928
1267
|
createRequestStorage: createRequestStorageWithContext,
|
|
929
1268
|
options,
|
|
930
|
-
|
|
1269
|
+
internalData: internalData as Record<string, unknown>,
|
|
931
1270
|
});
|
|
932
1271
|
}
|
|
933
1272
|
|
|
@@ -1003,8 +1342,7 @@ interface IFragnoInstantiatedFragment {
|
|
|
1003
1342
|
get $internal(): {
|
|
1004
1343
|
deps: unknown;
|
|
1005
1344
|
options: unknown;
|
|
1006
|
-
|
|
1007
|
-
};
|
|
1345
|
+
} & Record<string, unknown>;
|
|
1008
1346
|
|
|
1009
1347
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1010
1348
|
withMiddleware(handler: any): this;
|
|
@@ -1014,10 +1352,13 @@ interface IFragnoInstantiatedFragment {
|
|
|
1014
1352
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1015
1353
|
inContext<T>(callback: any): Promise<T>;
|
|
1016
1354
|
|
|
1355
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1356
|
+
callServices(serviceCalls: () => any): Promise<any>;
|
|
1357
|
+
|
|
1017
1358
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1018
1359
|
handlersFor(framework: FullstackFrameworks): any;
|
|
1019
1360
|
|
|
1020
|
-
handler(req: Request): Promise<Response>;
|
|
1361
|
+
handler(req: Request, lifecycleContext?: FragnoRequestLifecycleContext): Promise<Response>;
|
|
1021
1362
|
|
|
1022
1363
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1023
1364
|
callRoute(method: HTTPMethod, path: string, inputOptions?: any): Promise<any>;
|
|
@@ -1042,9 +1383,8 @@ export class FragmentInstantiationBuilder<
|
|
|
1042
1383
|
THandlerThisContext extends RequestThisContext,
|
|
1043
1384
|
TRequestStorage,
|
|
1044
1385
|
TRoutesOrFactories extends readonly AnyRouteOrFactory[],
|
|
1045
|
-
|
|
1046
|
-
> implements IFragmentInstantiationBuilder
|
|
1047
|
-
{
|
|
1386
|
+
TInternalRoutes extends readonly AnyRouteOrFactory[],
|
|
1387
|
+
> implements IFragmentInstantiationBuilder {
|
|
1048
1388
|
#definition: FragmentDefinition<
|
|
1049
1389
|
TConfig,
|
|
1050
1390
|
TOptions,
|
|
@@ -1056,7 +1396,7 @@ export class FragmentInstantiationBuilder<
|
|
|
1056
1396
|
TServiceThisContext,
|
|
1057
1397
|
THandlerThisContext,
|
|
1058
1398
|
TRequestStorage,
|
|
1059
|
-
|
|
1399
|
+
TInternalRoutes
|
|
1060
1400
|
>;
|
|
1061
1401
|
#config?: TConfig;
|
|
1062
1402
|
#routes?: TRoutesOrFactories;
|
|
@@ -1075,7 +1415,7 @@ export class FragmentInstantiationBuilder<
|
|
|
1075
1415
|
TServiceThisContext,
|
|
1076
1416
|
THandlerThisContext,
|
|
1077
1417
|
TRequestStorage,
|
|
1078
|
-
|
|
1418
|
+
TInternalRoutes
|
|
1079
1419
|
>,
|
|
1080
1420
|
routes?: TRoutesOrFactories,
|
|
1081
1421
|
) {
|
|
@@ -1097,7 +1437,7 @@ export class FragmentInstantiationBuilder<
|
|
|
1097
1437
|
TServiceThisContext,
|
|
1098
1438
|
THandlerThisContext,
|
|
1099
1439
|
TRequestStorage,
|
|
1100
|
-
|
|
1440
|
+
TInternalRoutes
|
|
1101
1441
|
> {
|
|
1102
1442
|
return this.#definition;
|
|
1103
1443
|
}
|
|
@@ -1148,7 +1488,7 @@ export class FragmentInstantiationBuilder<
|
|
|
1148
1488
|
THandlerThisContext,
|
|
1149
1489
|
TRequestStorage,
|
|
1150
1490
|
TNewRoutes,
|
|
1151
|
-
|
|
1491
|
+
TInternalRoutes
|
|
1152
1492
|
> {
|
|
1153
1493
|
const newBuilder = new FragmentInstantiationBuilder(this.#definition, routes);
|
|
1154
1494
|
// Preserve config, options, and services from the current instance
|
|
@@ -1178,14 +1518,13 @@ export class FragmentInstantiationBuilder<
|
|
|
1178
1518
|
* Build and return the instantiated fragment
|
|
1179
1519
|
*/
|
|
1180
1520
|
build(): FragnoInstantiatedFragment<
|
|
1181
|
-
FlattenRouteFactories<TRoutesOrFactories>,
|
|
1521
|
+
RoutesWithInternal<FlattenRouteFactories<TRoutesOrFactories>, TInternalRoutes>,
|
|
1182
1522
|
TDeps,
|
|
1183
1523
|
BoundServices<TBaseServices & TServices>,
|
|
1184
1524
|
TServiceThisContext,
|
|
1185
1525
|
THandlerThisContext,
|
|
1186
1526
|
TRequestStorage,
|
|
1187
|
-
TOptions
|
|
1188
|
-
TLinkedFragments
|
|
1527
|
+
TOptions
|
|
1189
1528
|
> {
|
|
1190
1529
|
// This variable is set by the frango-cli when extracting database schemas
|
|
1191
1530
|
const dryRun = process.env["FRAGNO_INIT_DRY_RUN"] === "true";
|
|
@@ -1224,7 +1563,7 @@ export function instantiate<
|
|
|
1224
1563
|
TServiceThisContext extends RequestThisContext,
|
|
1225
1564
|
THandlerThisContext extends RequestThisContext,
|
|
1226
1565
|
TRequestStorage,
|
|
1227
|
-
|
|
1566
|
+
TInternalRoutes extends readonly AnyRouteOrFactory[],
|
|
1228
1567
|
>(
|
|
1229
1568
|
definition: FragmentDefinition<
|
|
1230
1569
|
TConfig,
|
|
@@ -1237,7 +1576,7 @@ export function instantiate<
|
|
|
1237
1576
|
TServiceThisContext,
|
|
1238
1577
|
THandlerThisContext,
|
|
1239
1578
|
TRequestStorage,
|
|
1240
|
-
|
|
1579
|
+
TInternalRoutes
|
|
1241
1580
|
>,
|
|
1242
1581
|
): FragmentInstantiationBuilder<
|
|
1243
1582
|
TConfig,
|
|
@@ -1251,7 +1590,7 @@ export function instantiate<
|
|
|
1251
1590
|
THandlerThisContext,
|
|
1252
1591
|
TRequestStorage,
|
|
1253
1592
|
readonly [],
|
|
1254
|
-
|
|
1593
|
+
TInternalRoutes
|
|
1255
1594
|
> {
|
|
1256
1595
|
return new FragmentInstantiationBuilder(definition);
|
|
1257
1596
|
}
|