@fragno-dev/core 0.1.0 → 0.1.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 +18 -18
- package/CHANGELOG.md +12 -0
- package/dist/api/api.d.ts +1 -1
- package/dist/api/fragment-builder.d.ts +2 -2
- package/dist/api/fragment-instantiation.d.ts +2 -2
- package/dist/api/fragment-instantiation.js +2 -2
- package/dist/{api-Dcr4_-3g.d.ts → api-jKNXmz2B.d.ts} +92 -6
- package/dist/api-jKNXmz2B.d.ts.map +1 -0
- package/dist/client/client.d.ts +2 -2
- package/dist/client/client.js +2 -2
- package/dist/client/client.svelte.d.ts +2 -2
- package/dist/client/client.svelte.js +2 -2
- package/dist/client/react.d.ts +2 -2
- package/dist/client/react.d.ts.map +1 -1
- package/dist/client/react.js +2 -2
- package/dist/client/react.js.map +1 -1
- package/dist/client/solid.d.ts +2 -2
- package/dist/client/solid.js +2 -2
- package/dist/client/vanilla.d.ts +2 -2
- package/dist/client/vanilla.js +2 -2
- package/dist/client/vue.d.ts +2 -2
- package/dist/client/vue.js +2 -2
- package/dist/{client-CZCasGGB.js → client-CzWq6IlK.js} +4 -4
- package/dist/client-CzWq6IlK.js.map +1 -0
- package/dist/{fragment-builder-Dcdsms1l.d.ts → fragment-builder-B3JXWiZB.d.ts} +21 -6
- package/dist/{fragment-builder-Dcdsms1l.d.ts.map → fragment-builder-B3JXWiZB.d.ts.map} +1 -1
- package/dist/{fragment-instantiation-f4AhwQss.js → fragment-instantiation-D1q7pltx.js} +136 -11
- package/dist/fragment-instantiation-D1q7pltx.js.map +1 -0
- package/dist/mod.d.ts +2 -2
- package/dist/mod.js +2 -2
- package/dist/{route-B4RbOWjd.js → route-DbBZ3Ep9.js} +21 -13
- package/dist/route-DbBZ3Ep9.js.map +1 -0
- package/package.json +1 -1
- package/src/api/fragment-instantiation.ts +16 -3
- package/src/api/mutable-request-state.ts +107 -0
- package/src/api/request-input-context.test.ts +51 -0
- package/src/api/request-input-context.ts +20 -13
- package/src/api/request-middleware.test.ts +88 -2
- package/src/api/request-middleware.ts +28 -6
- package/src/api/request-output-context.test.ts +6 -2
- package/src/api/request-output-context.ts +15 -9
- package/src/client/client.ts +6 -3
- package/src/client/component.test.svelte +2 -0
- package/src/client/internal/ndjson-streaming.ts +6 -2
- package/src/client/react.ts +3 -1
- package/src/util/async.test.ts +6 -2
- package/.turbo/turbo-test.log +0 -297
- package/.turbo/turbo-types$colon$check.log +0 -1
- package/dist/api-Dcr4_-3g.d.ts.map +0 -1
- package/dist/client-CZCasGGB.js.map +0 -1
- package/dist/fragment-instantiation-f4AhwQss.js.map +0 -1
- package/dist/route-B4RbOWjd.js.map +0 -1
|
@@ -500,8 +500,7 @@ describe("Request Middleware", () => {
|
|
|
500
500
|
});
|
|
501
501
|
});
|
|
502
502
|
|
|
503
|
-
|
|
504
|
-
test.todo("middleware can modify query parameters", async () => {
|
|
503
|
+
test("middleware can modify query parameters", async () => {
|
|
505
504
|
const fragment = defineFragment("test-lib");
|
|
506
505
|
|
|
507
506
|
const routes = [
|
|
@@ -542,4 +541,91 @@ describe("Request Middleware", () => {
|
|
|
542
541
|
role: "some-other-role-defined-in-middleware",
|
|
543
542
|
});
|
|
544
543
|
});
|
|
544
|
+
|
|
545
|
+
test("middleware can modify request body", async () => {
|
|
546
|
+
const fragment = defineFragment("test-lib");
|
|
547
|
+
|
|
548
|
+
const routes = [
|
|
549
|
+
defineRoute({
|
|
550
|
+
method: "POST",
|
|
551
|
+
path: "/users",
|
|
552
|
+
inputSchema: z.object({ name: z.string(), role: z.string().optional() }),
|
|
553
|
+
outputSchema: z.object({ name: z.string(), role: z.string() }),
|
|
554
|
+
handler: async ({ input }, { json }) => {
|
|
555
|
+
const body = await input.valid();
|
|
556
|
+
return json({
|
|
557
|
+
name: body.name,
|
|
558
|
+
role: body.role ?? "user",
|
|
559
|
+
});
|
|
560
|
+
},
|
|
561
|
+
}),
|
|
562
|
+
] as const;
|
|
563
|
+
|
|
564
|
+
const instance = createFragment(fragment, {}, routes, {
|
|
565
|
+
mountRoute: "/api",
|
|
566
|
+
}).withMiddleware(async ({ ifMatchesRoute, requestState }) => {
|
|
567
|
+
// Middleware modifies the request body
|
|
568
|
+
const result = await ifMatchesRoute("POST", "/users", async ({ input }) => {
|
|
569
|
+
const body = await input.valid();
|
|
570
|
+
// Modify the body by adding a role field
|
|
571
|
+
requestState.setBody({
|
|
572
|
+
...body,
|
|
573
|
+
role: "admin-from-middleware",
|
|
574
|
+
});
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
return result;
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
const req = new Request("http://localhost/api/users", {
|
|
581
|
+
method: "POST",
|
|
582
|
+
headers: { "Content-Type": "application/json" },
|
|
583
|
+
body: JSON.stringify({ name: "John Doe" }),
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
const res = await instance.handler(req);
|
|
587
|
+
expect(res.status).toBe(200);
|
|
588
|
+
expect(await res.json()).toEqual({
|
|
589
|
+
name: "John Doe",
|
|
590
|
+
role: "admin-from-middleware",
|
|
591
|
+
});
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
test("middleware can modify request headers", async () => {
|
|
595
|
+
const fragment = defineFragment("test-lib");
|
|
596
|
+
|
|
597
|
+
const routes = [
|
|
598
|
+
defineRoute({
|
|
599
|
+
method: "GET",
|
|
600
|
+
path: "/data",
|
|
601
|
+
outputSchema: z.object({ auth: z.string(), custom: z.string() }),
|
|
602
|
+
handler: async ({ headers }, { json }) => {
|
|
603
|
+
return json({
|
|
604
|
+
auth: headers.get("Authorization") ?? "none",
|
|
605
|
+
custom: headers.get("X-Custom-Header") ?? "none",
|
|
606
|
+
});
|
|
607
|
+
},
|
|
608
|
+
}),
|
|
609
|
+
] as const;
|
|
610
|
+
|
|
611
|
+
const instance = createFragment(fragment, {}, routes, {
|
|
612
|
+
mountRoute: "/api",
|
|
613
|
+
}).withMiddleware(async ({ headers }) => {
|
|
614
|
+
// Middleware modifies headers
|
|
615
|
+
headers.set("Authorization", "Bearer middleware-token");
|
|
616
|
+
headers.set("X-Custom-Header", "middleware-value");
|
|
617
|
+
return undefined;
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
const req = new Request("http://localhost/api/data", {
|
|
621
|
+
method: "GET",
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
const res = await instance.handler(req);
|
|
625
|
+
expect(res.status).toBe(200);
|
|
626
|
+
expect(await res.json()).toEqual({
|
|
627
|
+
auth: "Bearer middleware-token",
|
|
628
|
+
custom: "middleware-value",
|
|
629
|
+
});
|
|
630
|
+
});
|
|
545
631
|
});
|
|
@@ -2,10 +2,10 @@ import type { StandardSchemaV1 } from "@standard-schema/spec";
|
|
|
2
2
|
import type { ExtractRouteByPath, ExtractRoutePath } from "../client/client";
|
|
3
3
|
import type { HTTPMethod } from "./api";
|
|
4
4
|
import type { ExtractPathParams } from "./internal/path";
|
|
5
|
-
import type { RequestBodyType } from "./request-input-context";
|
|
6
5
|
import type { AnyFragnoRouteConfig } from "./route";
|
|
7
6
|
import { RequestInputContext } from "./request-input-context";
|
|
8
7
|
import { OutputContext, RequestOutputContext } from "./request-output-context";
|
|
8
|
+
import { MutableRequestState } from "./mutable-request-state";
|
|
9
9
|
|
|
10
10
|
export type FragnoMiddlewareCallback<
|
|
11
11
|
TRoutes extends readonly AnyFragnoRouteConfig[],
|
|
@@ -19,10 +19,8 @@ export type FragnoMiddlewareCallback<
|
|
|
19
19
|
export interface RequestMiddlewareOptions {
|
|
20
20
|
path: string;
|
|
21
21
|
method: HTTPMethod;
|
|
22
|
-
pathParams?: Record<string, string>;
|
|
23
|
-
searchParams: URLSearchParams;
|
|
24
|
-
body: RequestBodyType;
|
|
25
22
|
request: Request;
|
|
23
|
+
state: MutableRequestState;
|
|
26
24
|
}
|
|
27
25
|
|
|
28
26
|
export class RequestMiddlewareOutputContext<
|
|
@@ -50,9 +48,11 @@ export class RequestMiddlewareOutputContext<
|
|
|
50
48
|
export class RequestMiddlewareInputContext<const TRoutes extends readonly AnyFragnoRouteConfig[]> {
|
|
51
49
|
readonly #options: RequestMiddlewareOptions;
|
|
52
50
|
readonly #route: TRoutes[number];
|
|
51
|
+
readonly #state: MutableRequestState;
|
|
53
52
|
|
|
54
53
|
constructor(routes: TRoutes, options: RequestMiddlewareOptions) {
|
|
55
54
|
this.#options = options;
|
|
55
|
+
this.#state = options.state;
|
|
56
56
|
|
|
57
57
|
const route = routes.find(
|
|
58
58
|
(route) => route.path === options.path && route.method === options.method,
|
|
@@ -74,11 +74,15 @@ export class RequestMiddlewareInputContext<const TRoutes extends readonly AnyFra
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
get pathParams(): Record<string, string> {
|
|
77
|
-
return this.#
|
|
77
|
+
return this.#state.pathParams;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
get queryParams(): URLSearchParams {
|
|
81
|
-
return this.#
|
|
81
|
+
return this.#state.searchParams;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
get headers(): Headers {
|
|
85
|
+
return this.#state.headers;
|
|
82
86
|
}
|
|
83
87
|
|
|
84
88
|
get inputSchema(): StandardSchemaV1 | undefined {
|
|
@@ -89,6 +93,23 @@ export class RequestMiddlewareInputContext<const TRoutes extends readonly AnyFra
|
|
|
89
93
|
return this.#route.outputSchema;
|
|
90
94
|
}
|
|
91
95
|
|
|
96
|
+
/**
|
|
97
|
+
* Access to the mutable request state.
|
|
98
|
+
* Use this to modify query parameters, path parameters, or request body.
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* ```typescript
|
|
102
|
+
* // Modify body
|
|
103
|
+
* requestState.setBody({ modified: true });
|
|
104
|
+
*
|
|
105
|
+
* // Query params are already accessible via queryParams getter
|
|
106
|
+
* // Path params are already accessible via pathParams getter
|
|
107
|
+
* ```
|
|
108
|
+
*/
|
|
109
|
+
get requestState(): MutableRequestState {
|
|
110
|
+
return this.#state;
|
|
111
|
+
}
|
|
112
|
+
|
|
92
113
|
// Defined as a field so that `this` reference stays in tact when destructuring
|
|
93
114
|
ifMatchesRoute = async <
|
|
94
115
|
const TMethod extends HTTPMethod,
|
|
@@ -116,6 +137,7 @@ export class RequestMiddlewareInputContext<const TRoutes extends readonly AnyFra
|
|
|
116
137
|
path: path,
|
|
117
138
|
pathParams: this.pathParams as ExtractPathParams<TPath>,
|
|
118
139
|
inputSchema: this.#route.inputSchema,
|
|
140
|
+
state: this.#state,
|
|
119
141
|
});
|
|
120
142
|
|
|
121
143
|
const outputContext = new RequestOutputContext(this.#route.outputSchema);
|
|
@@ -327,7 +327,9 @@ describe("RequestOutputContext", () => {
|
|
|
327
327
|
try {
|
|
328
328
|
while (true) {
|
|
329
329
|
const { done, value } = await reader.read();
|
|
330
|
-
if (done)
|
|
330
|
+
if (done) {
|
|
331
|
+
break;
|
|
332
|
+
}
|
|
331
333
|
chunks.push(decoder.decode(value));
|
|
332
334
|
}
|
|
333
335
|
} catch {
|
|
@@ -354,7 +356,9 @@ describe("RequestOutputContext", () => {
|
|
|
354
356
|
try {
|
|
355
357
|
while (true) {
|
|
356
358
|
const { done, value } = await reader.read();
|
|
357
|
-
if (done)
|
|
359
|
+
if (done) {
|
|
360
|
+
break;
|
|
361
|
+
}
|
|
358
362
|
chunks.push(decoder.decode(value));
|
|
359
363
|
}
|
|
360
364
|
} catch {
|
|
@@ -19,7 +19,9 @@ function mergeHeaders(...headerSources: (HeadersInit | undefined)[]): HeadersIni
|
|
|
19
19
|
const mergedHeaders = new Headers();
|
|
20
20
|
|
|
21
21
|
for (const headerSource of headerSources) {
|
|
22
|
-
if (!headerSource)
|
|
22
|
+
if (!headerSource) {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
23
25
|
|
|
24
26
|
if (headerSource instanceof Headers) {
|
|
25
27
|
for (const [key, value] of headerSource.entries()) {
|
|
@@ -45,11 +47,11 @@ export abstract class OutputContext<const TOutput, const TErrorCode extends stri
|
|
|
45
47
|
*
|
|
46
48
|
* Shortcut for `throw new FragnoApiError(...)`
|
|
47
49
|
*/
|
|
48
|
-
error(
|
|
50
|
+
error = (
|
|
49
51
|
{ message, code }: { message: string; code: TErrorCode },
|
|
50
52
|
initOrStatus?: ResponseInit | StatusCode,
|
|
51
53
|
headers?: HeadersInit,
|
|
52
|
-
): Response {
|
|
54
|
+
): Response => {
|
|
53
55
|
if (typeof initOrStatus === "undefined") {
|
|
54
56
|
return Response.json({ message: message, code }, { status: 500, headers });
|
|
55
57
|
}
|
|
@@ -63,12 +65,12 @@ export abstract class OutputContext<const TOutput, const TErrorCode extends stri
|
|
|
63
65
|
{ message: message, code },
|
|
64
66
|
{ status: initOrStatus.status, headers: mergedHeaders },
|
|
65
67
|
);
|
|
66
|
-
}
|
|
68
|
+
};
|
|
67
69
|
|
|
68
|
-
empty(
|
|
70
|
+
empty = (
|
|
69
71
|
initOrStatus?: ResponseInit<ContentlessStatusCode> | ContentlessStatusCode,
|
|
70
72
|
headers?: HeadersInit,
|
|
71
|
-
): Response {
|
|
73
|
+
): Response => {
|
|
72
74
|
const defaultHeaders = {};
|
|
73
75
|
|
|
74
76
|
if (typeof initOrStatus === "undefined") {
|
|
@@ -92,9 +94,13 @@ export abstract class OutputContext<const TOutput, const TErrorCode extends stri
|
|
|
92
94
|
status: initOrStatus.status,
|
|
93
95
|
headers: mergedHeaders,
|
|
94
96
|
});
|
|
95
|
-
}
|
|
97
|
+
};
|
|
96
98
|
|
|
97
|
-
json
|
|
99
|
+
json = (
|
|
100
|
+
object: TOutput,
|
|
101
|
+
initOrStatus?: ResponseInit | StatusCode,
|
|
102
|
+
headers?: HeadersInit,
|
|
103
|
+
): Response => {
|
|
98
104
|
if (typeof initOrStatus === "undefined") {
|
|
99
105
|
return Response.json(object, {
|
|
100
106
|
status: 200,
|
|
@@ -114,7 +120,7 @@ export abstract class OutputContext<const TOutput, const TErrorCode extends stri
|
|
|
114
120
|
status: initOrStatus.status,
|
|
115
121
|
headers: mergedHeaders,
|
|
116
122
|
});
|
|
117
|
-
}
|
|
123
|
+
};
|
|
118
124
|
|
|
119
125
|
jsonStream = (
|
|
120
126
|
cb: (stream: ResponseStream<TOutput>) => void | Promise<void>,
|
package/src/client/client.ts
CHANGED
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
} from "./internal/ndjson-streaming";
|
|
26
26
|
import { addStore, getInitialData, SSR_ENABLED } from "../util/ssr";
|
|
27
27
|
import { unwrapObject } from "../util/nanostores";
|
|
28
|
-
import type {
|
|
28
|
+
import type { FragmentDefinition } from "../api/fragment-builder";
|
|
29
29
|
import {
|
|
30
30
|
type AnyRouteOrFactory,
|
|
31
31
|
type FlattenRouteFactories,
|
|
@@ -1001,15 +1001,18 @@ export function createClientBuilder<
|
|
|
1001
1001
|
TDeps,
|
|
1002
1002
|
TServices extends Record<string, unknown>,
|
|
1003
1003
|
const TRoutesOrFactories extends readonly AnyRouteOrFactory[],
|
|
1004
|
+
const TAdditionalContext extends Record<string, unknown>,
|
|
1004
1005
|
>(
|
|
1005
|
-
|
|
1006
|
+
fragmentBuilder: {
|
|
1007
|
+
definition: FragmentDefinition<TConfig, TDeps, TServices, TAdditionalContext>;
|
|
1008
|
+
},
|
|
1006
1009
|
publicConfig: FragnoPublicClientConfig,
|
|
1007
1010
|
routesOrFactories: TRoutesOrFactories,
|
|
1008
1011
|
): ClientBuilder<
|
|
1009
1012
|
FlattenRouteFactories<TRoutesOrFactories>,
|
|
1010
1013
|
FragnoFragmentSharedConfig<FlattenRouteFactories<TRoutesOrFactories>>
|
|
1011
1014
|
> {
|
|
1012
|
-
const definition =
|
|
1015
|
+
const definition = fragmentBuilder.definition;
|
|
1013
1016
|
|
|
1014
1017
|
// For client-side, we resolve route factories with dummy context
|
|
1015
1018
|
// This will be removed by the bundle plugin anyway
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
import type { Readable } from "svelte/store";
|
|
3
3
|
import { useFragno } from "./client.svelte";
|
|
4
4
|
|
|
5
|
+
// oxlint-disable-next-line no-unassigned-vars
|
|
5
6
|
export let clientObj: Record<string, unknown>;
|
|
7
|
+
// oxlint-disable-next-line no-unassigned-vars
|
|
6
8
|
export let hookName: string;
|
|
7
9
|
export let args: Record<string, unknown> = {};
|
|
8
10
|
|
|
@@ -191,7 +191,9 @@ async function continueStreaming<TOutputSchema extends StandardSchemaV1, TErrorC
|
|
|
191
191
|
if (buffer.trim()) {
|
|
192
192
|
const lines = buffer.split("\n");
|
|
193
193
|
for (const line of lines) {
|
|
194
|
-
if (!line.trim())
|
|
194
|
+
if (!line.trim()) {
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
195
197
|
|
|
196
198
|
try {
|
|
197
199
|
const jsonObject = JSON.parse(line) as StandardSchemaV1.InferOutput<TOutputSchema>;
|
|
@@ -215,7 +217,9 @@ async function continueStreaming<TOutputSchema extends StandardSchemaV1, TErrorC
|
|
|
215
217
|
buffer = lines.pop() || ""; // Keep incomplete line in buffer
|
|
216
218
|
|
|
217
219
|
for (const line of lines) {
|
|
218
|
-
if (!line.trim())
|
|
220
|
+
if (!line.trim()) {
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
219
223
|
|
|
220
224
|
try {
|
|
221
225
|
const jsonObject = JSON.parse(line) as StandardSchemaV1.InferOutput<TOutputSchema>;
|
package/src/client/react.ts
CHANGED
|
@@ -210,7 +210,9 @@ export function useStore<SomeStore extends Store>(
|
|
|
210
210
|
|
|
211
211
|
const subscribe = useCallback((onChange: () => void) => {
|
|
212
212
|
const emitChange = (value: StoreValue<SomeStore>) => {
|
|
213
|
-
if (snapshotRef.current === value)
|
|
213
|
+
if (snapshotRef.current === value) {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
214
216
|
snapshotRef.current = value;
|
|
215
217
|
onChange();
|
|
216
218
|
};
|
package/src/util/async.test.ts
CHANGED
|
@@ -20,7 +20,9 @@ describe("createAsyncIteratorFromCallback", () => {
|
|
|
20
20
|
const values: string[] = [];
|
|
21
21
|
for await (const value of iterator) {
|
|
22
22
|
values.push(value);
|
|
23
|
-
if (values.length === 3)
|
|
23
|
+
if (values.length === 3) {
|
|
24
|
+
break;
|
|
25
|
+
}
|
|
24
26
|
}
|
|
25
27
|
return values;
|
|
26
28
|
})();
|
|
@@ -53,7 +55,9 @@ describe("createAsyncIteratorFromCallback", () => {
|
|
|
53
55
|
const values: string[] = [];
|
|
54
56
|
for await (const value of iterator) {
|
|
55
57
|
values.push(value);
|
|
56
|
-
if (values.length === 2)
|
|
58
|
+
if (values.length === 2) {
|
|
59
|
+
break;
|
|
60
|
+
} // Break after 2 values
|
|
57
61
|
}
|
|
58
62
|
return values;
|
|
59
63
|
})();
|