@fragno-dev/core 0.0.1
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 +61 -0
- package/.turbo/turbo-types$colon$check.log +2 -0
- package/dist/api/api.d.ts +2 -0
- package/dist/api/api.js +3 -0
- package/dist/api-CBDGZiLC.d.ts +278 -0
- package/dist/api-CBDGZiLC.d.ts.map +1 -0
- package/dist/api-DgHfYjq2.js +54 -0
- package/dist/api-DgHfYjq2.js.map +1 -0
- package/dist/client/client.d.ts +3 -0
- package/dist/client/client.js +6 -0
- package/dist/client/client.svelte.d.ts +33 -0
- package/dist/client/client.svelte.d.ts.map +1 -0
- package/dist/client/client.svelte.js +123 -0
- package/dist/client/client.svelte.js.map +1 -0
- package/dist/client/react.d.ts +58 -0
- package/dist/client/react.d.ts.map +1 -0
- package/dist/client/react.js +80 -0
- package/dist/client/react.js.map +1 -0
- package/dist/client/vanilla.d.ts +61 -0
- package/dist/client/vanilla.d.ts.map +1 -0
- package/dist/client/vanilla.js +136 -0
- package/dist/client/vanilla.js.map +1 -0
- package/dist/client/vue.d.ts +39 -0
- package/dist/client/vue.d.ts.map +1 -0
- package/dist/client/vue.js +108 -0
- package/dist/client/vue.js.map +1 -0
- package/dist/client-DWjxKDnE.js +703 -0
- package/dist/client-DWjxKDnE.js.map +1 -0
- package/dist/client-XFdAy-IQ.d.ts +287 -0
- package/dist/client-XFdAy-IQ.d.ts.map +1 -0
- package/dist/integrations/astro.d.ts +18 -0
- package/dist/integrations/astro.d.ts.map +1 -0
- package/dist/integrations/astro.js +16 -0
- package/dist/integrations/astro.js.map +1 -0
- package/dist/integrations/next-js.d.ts +15 -0
- package/dist/integrations/next-js.d.ts.map +1 -0
- package/dist/integrations/next-js.js +17 -0
- package/dist/integrations/next-js.js.map +1 -0
- package/dist/integrations/react-ssr.d.ts +19 -0
- package/dist/integrations/react-ssr.d.ts.map +1 -0
- package/dist/integrations/react-ssr.js +38 -0
- package/dist/integrations/react-ssr.js.map +1 -0
- package/dist/integrations/svelte-kit.d.ts +21 -0
- package/dist/integrations/svelte-kit.d.ts.map +1 -0
- package/dist/integrations/svelte-kit.js +18 -0
- package/dist/integrations/svelte-kit.js.map +1 -0
- package/dist/mod.d.ts +3 -0
- package/dist/mod.js +177 -0
- package/dist/mod.js.map +1 -0
- package/dist/route-Bp6eByhz.js +331 -0
- package/dist/route-Bp6eByhz.js.map +1 -0
- package/dist/ssr-tJHqcNSw.js +48 -0
- package/dist/ssr-tJHqcNSw.js.map +1 -0
- package/package.json +127 -0
- package/src/api/api.test.ts +140 -0
- package/src/api/api.ts +106 -0
- package/src/api/error.ts +47 -0
- package/src/api/fragment.test.ts +509 -0
- package/src/api/fragment.ts +277 -0
- package/src/api/internal/path-runtime.test.ts +121 -0
- package/src/api/internal/path-type.test.ts +602 -0
- package/src/api/internal/path.ts +322 -0
- package/src/api/internal/response-stream.ts +118 -0
- package/src/api/internal/route.test.ts +56 -0
- package/src/api/internal/route.ts +9 -0
- package/src/api/request-input-context.test.ts +437 -0
- package/src/api/request-input-context.ts +201 -0
- package/src/api/request-middleware.test.ts +544 -0
- package/src/api/request-middleware.ts +126 -0
- package/src/api/request-output-context.test.ts +626 -0
- package/src/api/request-output-context.ts +175 -0
- package/src/api/route.test.ts +176 -0
- package/src/api/route.ts +152 -0
- package/src/client/client-builder.test.ts +264 -0
- package/src/client/client-error.test.ts +15 -0
- package/src/client/client-error.ts +141 -0
- package/src/client/client-types.test.ts +493 -0
- package/src/client/client.ssr.test.ts +173 -0
- package/src/client/client.svelte.test.ts +837 -0
- package/src/client/client.svelte.ts +278 -0
- package/src/client/client.test.ts +1690 -0
- package/src/client/client.ts +1035 -0
- package/src/client/component.test.svelte +21 -0
- package/src/client/internal/ndjson-streaming.test.ts +457 -0
- package/src/client/internal/ndjson-streaming.ts +248 -0
- package/src/client/react.test.ts +947 -0
- package/src/client/react.ts +241 -0
- package/src/client/vanilla.test.ts +867 -0
- package/src/client/vanilla.ts +265 -0
- package/src/client/vue.test.ts +754 -0
- package/src/client/vue.ts +242 -0
- package/src/http/http-status.ts +60 -0
- package/src/integrations/astro.ts +17 -0
- package/src/integrations/next-js.ts +31 -0
- package/src/integrations/react-ssr.ts +40 -0
- package/src/integrations/svelte-kit.ts +41 -0
- package/src/mod.ts +20 -0
- package/src/util/async.test.ts +85 -0
- package/src/util/async.ts +96 -0
- package/src/util/content-type.test.ts +136 -0
- package/src/util/content-type.ts +84 -0
- package/src/util/nanostores.test.ts +28 -0
- package/src/util/nanostores.ts +65 -0
- package/src/util/ssr.ts +75 -0
- package/src/util/types-util.ts +16 -0
- package/tsconfig.json +10 -0
- package/tsdown.config.ts +21 -0
- package/vitest.config.ts +10 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import type { StandardSchemaV1 } from "@standard-schema/spec";
|
|
2
|
+
import type { ContentlessStatusCode, StatusCode } from "../http/http-status";
|
|
3
|
+
import { ResponseStream } from "./internal/response-stream";
|
|
4
|
+
import type { InferOrUnknown } from "../util/types-util";
|
|
5
|
+
|
|
6
|
+
export type ResponseData = string | ArrayBuffer | ReadableStream | Uint8Array<ArrayBuffer>;
|
|
7
|
+
|
|
8
|
+
interface ResponseInit<T extends StatusCode = StatusCode> {
|
|
9
|
+
headers?: HeadersInit;
|
|
10
|
+
status?: T;
|
|
11
|
+
statusText?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Utility function to merge headers from multiple sources.
|
|
16
|
+
* Later headers override earlier ones.
|
|
17
|
+
*/
|
|
18
|
+
function mergeHeaders(...headerSources: (HeadersInit | undefined)[]): HeadersInit | undefined {
|
|
19
|
+
const mergedHeaders = new Headers();
|
|
20
|
+
|
|
21
|
+
for (const headerSource of headerSources) {
|
|
22
|
+
if (!headerSource) continue;
|
|
23
|
+
|
|
24
|
+
if (headerSource instanceof Headers) {
|
|
25
|
+
for (const [key, value] of headerSource.entries()) {
|
|
26
|
+
mergedHeaders.set(key, value);
|
|
27
|
+
}
|
|
28
|
+
} else if (Array.isArray(headerSource)) {
|
|
29
|
+
for (const [key, value] of headerSource) {
|
|
30
|
+
mergedHeaders.set(key, value);
|
|
31
|
+
}
|
|
32
|
+
} else {
|
|
33
|
+
for (const [key, value] of Object.entries(headerSource)) {
|
|
34
|
+
mergedHeaders.set(key, value);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return mergedHeaders;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export abstract class OutputContext<const TOutput, const TErrorCode extends string> {
|
|
43
|
+
/**
|
|
44
|
+
* Creates an error response.
|
|
45
|
+
*
|
|
46
|
+
* Shortcut for `throw new FragnoApiError(...)`
|
|
47
|
+
*/
|
|
48
|
+
error(
|
|
49
|
+
{ message, code }: { message: string; code: TErrorCode },
|
|
50
|
+
initOrStatus?: ResponseInit | StatusCode,
|
|
51
|
+
headers?: HeadersInit,
|
|
52
|
+
): Response {
|
|
53
|
+
if (typeof initOrStatus === "undefined") {
|
|
54
|
+
return Response.json({ error: message, code }, { status: 500, headers });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (typeof initOrStatus === "number") {
|
|
58
|
+
return Response.json({ error: message, code }, { status: initOrStatus, headers });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const mergedHeaders = mergeHeaders(initOrStatus.headers, headers);
|
|
62
|
+
return Response.json(
|
|
63
|
+
{ error: message, code },
|
|
64
|
+
{ status: initOrStatus.status, headers: mergedHeaders },
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
empty(
|
|
69
|
+
initOrStatus?: ResponseInit<ContentlessStatusCode> | ContentlessStatusCode,
|
|
70
|
+
headers?: HeadersInit,
|
|
71
|
+
): Response {
|
|
72
|
+
const defaultHeaders = {};
|
|
73
|
+
|
|
74
|
+
if (typeof initOrStatus === "undefined") {
|
|
75
|
+
const mergedHeaders = mergeHeaders(defaultHeaders, headers);
|
|
76
|
+
return Response.json(null, {
|
|
77
|
+
status: 201,
|
|
78
|
+
headers: mergedHeaders,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (typeof initOrStatus === "number") {
|
|
83
|
+
const mergedHeaders = mergeHeaders(defaultHeaders, headers);
|
|
84
|
+
return Response.json(null, {
|
|
85
|
+
status: initOrStatus,
|
|
86
|
+
headers: mergedHeaders,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const mergedHeaders = mergeHeaders(defaultHeaders, initOrStatus.headers, headers);
|
|
91
|
+
return Response.json(null, {
|
|
92
|
+
status: initOrStatus.status,
|
|
93
|
+
headers: mergedHeaders,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
json(object: TOutput, initOrStatus?: ResponseInit | StatusCode, headers?: HeadersInit): Response {
|
|
98
|
+
if (typeof initOrStatus === "undefined") {
|
|
99
|
+
return Response.json(object, {
|
|
100
|
+
status: 200,
|
|
101
|
+
headers,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (typeof initOrStatus === "number") {
|
|
106
|
+
return Response.json(object, {
|
|
107
|
+
status: initOrStatus,
|
|
108
|
+
headers,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const mergedHeaders = mergeHeaders(initOrStatus.headers, headers);
|
|
113
|
+
return Response.json(object, {
|
|
114
|
+
status: initOrStatus.status,
|
|
115
|
+
headers: mergedHeaders,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
jsonStream = (
|
|
120
|
+
cb: (stream: ResponseStream<TOutput>) => void | Promise<void>,
|
|
121
|
+
{
|
|
122
|
+
onError,
|
|
123
|
+
headers,
|
|
124
|
+
}: {
|
|
125
|
+
onError?: (error: Error, stream: ResponseStream<TOutput>) => void | Promise<void>;
|
|
126
|
+
headers?: HeadersInit;
|
|
127
|
+
} = {},
|
|
128
|
+
): Response => {
|
|
129
|
+
// Note: this is intentionally an arrow function (=>) to keep `this` context.
|
|
130
|
+
const defaultHeaders = {
|
|
131
|
+
"content-type": "application/x-ndjson; charset=utf-8",
|
|
132
|
+
"transfer-encoding": "chunked",
|
|
133
|
+
"cache-control": "no-cache",
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const { readable, writable } = new TransformStream();
|
|
137
|
+
const stream = new ResponseStream(writable, readable);
|
|
138
|
+
|
|
139
|
+
(async () => {
|
|
140
|
+
try {
|
|
141
|
+
await cb(stream);
|
|
142
|
+
} catch (e) {
|
|
143
|
+
if (e === undefined) {
|
|
144
|
+
// If reading is canceled without a reason value (e.g. by StreamingApi)
|
|
145
|
+
// then the .pipeTo() promise will reject with undefined.
|
|
146
|
+
// In this case, do nothing because the stream is already closed.
|
|
147
|
+
} else if (e instanceof Error && onError) {
|
|
148
|
+
await onError(e, stream);
|
|
149
|
+
} else {
|
|
150
|
+
console.error(e);
|
|
151
|
+
}
|
|
152
|
+
} finally {
|
|
153
|
+
stream.close();
|
|
154
|
+
}
|
|
155
|
+
})();
|
|
156
|
+
|
|
157
|
+
return new Response(stream.responseReadable, {
|
|
158
|
+
status: 200,
|
|
159
|
+
headers: mergeHeaders(defaultHeaders, headers),
|
|
160
|
+
});
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export class RequestOutputContext<
|
|
165
|
+
const TOutputSchema extends StandardSchemaV1 | undefined = undefined,
|
|
166
|
+
const TErrorCode extends string = string,
|
|
167
|
+
> extends OutputContext<InferOrUnknown<TOutputSchema>, TErrorCode> {
|
|
168
|
+
// eslint-disable-next-line no-unused-private-class-members
|
|
169
|
+
#outputSchema?: TOutputSchema;
|
|
170
|
+
|
|
171
|
+
constructor(outputSchema?: TOutputSchema) {
|
|
172
|
+
super();
|
|
173
|
+
this.#outputSchema = outputSchema;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { test, expect, expectTypeOf, describe } from "vitest";
|
|
2
|
+
import { defineRoute } from "./route";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
|
|
5
|
+
describe("defineRoute", () => {
|
|
6
|
+
test("defineRoute no inputSchema", () => {
|
|
7
|
+
const route = defineRoute({
|
|
8
|
+
method: "GET",
|
|
9
|
+
path: "/thing/**:path",
|
|
10
|
+
handler: async ({ path, pathParams }, { empty }) => {
|
|
11
|
+
expect(path).toEqual("/thing/**:path");
|
|
12
|
+
expectTypeOf<typeof path>().toEqualTypeOf<"/thing/**:path">();
|
|
13
|
+
|
|
14
|
+
expect(pathParams).toEqual({ path: "test" });
|
|
15
|
+
expectTypeOf<typeof pathParams>().toEqualTypeOf<{ path: string }>();
|
|
16
|
+
return empty();
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
expect(route.method).toBe("GET");
|
|
21
|
+
expect(route.path).toBe("/thing/**:path");
|
|
22
|
+
expect(route.handler).toBeDefined();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("defineRoute with inputSchema", () => {
|
|
26
|
+
const route = defineRoute({
|
|
27
|
+
method: "GET" as const,
|
|
28
|
+
path: "/thing/**:path" as const,
|
|
29
|
+
inputSchema: z.object({
|
|
30
|
+
path: z.string(),
|
|
31
|
+
}),
|
|
32
|
+
handler: async ({ path, pathParams, input }, { empty }) => {
|
|
33
|
+
expect(path).toEqual("/thing/**:path");
|
|
34
|
+
expectTypeOf<typeof path>().toEqualTypeOf<"/thing/**:path">();
|
|
35
|
+
|
|
36
|
+
expect(pathParams).toEqual({ path: "test" });
|
|
37
|
+
expectTypeOf<typeof pathParams>().toEqualTypeOf<{ path: string }>();
|
|
38
|
+
|
|
39
|
+
expect(input).toBeTruthy();
|
|
40
|
+
if (input) {
|
|
41
|
+
expectTypeOf<typeof input.schema>().toEqualTypeOf<z.ZodObject<{ path: z.ZodString }>>();
|
|
42
|
+
expectTypeOf<typeof input.valid>().toEqualTypeOf<() => Promise<{ path: string }>>();
|
|
43
|
+
|
|
44
|
+
const _valid = await input.valid();
|
|
45
|
+
expectTypeOf<typeof _valid>().toEqualTypeOf<{ path: string }>();
|
|
46
|
+
}
|
|
47
|
+
return empty();
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
expect(route.method).toBe("GET");
|
|
52
|
+
expect(route.path).toBe("/thing/**:path");
|
|
53
|
+
expect(route.inputSchema).toBeDefined();
|
|
54
|
+
expect(route.handler).toBeDefined();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("HTTPMethod DELETE without inputSchema or outputSchema", () => {
|
|
58
|
+
const route = defineRoute({
|
|
59
|
+
method: "DELETE",
|
|
60
|
+
path: "/thing",
|
|
61
|
+
handler: async ({ input }, { empty, json }) => {
|
|
62
|
+
// FIXME: Would be nicer if input was not on the object at all
|
|
63
|
+
expect(input).toBeUndefined();
|
|
64
|
+
expectTypeOf<typeof input>().toEqualTypeOf<undefined>();
|
|
65
|
+
|
|
66
|
+
// FIXME: Would be nicer if parameter of json was never, or not have json as field at all.
|
|
67
|
+
expect(json).toBeDefined();
|
|
68
|
+
expectTypeOf<Parameters<typeof json>[0]>().toEqualTypeOf<unknown>();
|
|
69
|
+
|
|
70
|
+
return empty();
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
expect(route.method).toBe("DELETE");
|
|
75
|
+
expect(route.path).toBe("/thing");
|
|
76
|
+
expect(route.handler).toBeDefined();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("defineRoute with outputSchema", () => {
|
|
80
|
+
const route = defineRoute({
|
|
81
|
+
method: "GET",
|
|
82
|
+
path: "/users",
|
|
83
|
+
outputSchema: z.array(z.object({ id: z.number(), name: z.string() })),
|
|
84
|
+
handler: async (_ctx, { json }) => {
|
|
85
|
+
return json([{ id: 1, name: "John" }]);
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
expect(route.method).toBe("GET");
|
|
90
|
+
expect(route.path).toBe("/users");
|
|
91
|
+
expect(route.outputSchema).toBeDefined();
|
|
92
|
+
expect(route.handler).toBeDefined();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("defineRoute with both inputSchema and outputSchema", () => {
|
|
96
|
+
const route = defineRoute({
|
|
97
|
+
method: "POST",
|
|
98
|
+
path: "/users",
|
|
99
|
+
inputSchema: z.object({ name: z.string(), email: z.string() }),
|
|
100
|
+
outputSchema: z.object({ id: z.number(), name: z.string(), email: z.string() }),
|
|
101
|
+
handler: async ({ input }, { json }) => {
|
|
102
|
+
expect(input).toBeTruthy();
|
|
103
|
+
if (input) {
|
|
104
|
+
const data = await input.valid();
|
|
105
|
+
return json({ id: 1, name: data.name, email: data.email });
|
|
106
|
+
}
|
|
107
|
+
return json({ id: 1, name: "", email: "" });
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
expect(route.method).toBe("POST");
|
|
112
|
+
expect(route.path).toBe("/users");
|
|
113
|
+
expect(route.inputSchema).toBeDefined();
|
|
114
|
+
expect(route.outputSchema).toBeDefined();
|
|
115
|
+
expect(route.handler).toBeDefined();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("defineRoute with path parameters", () => {
|
|
119
|
+
const route = defineRoute({
|
|
120
|
+
method: "GET",
|
|
121
|
+
path: "/users/:id",
|
|
122
|
+
outputSchema: z.object({ id: z.number(), name: z.string() }),
|
|
123
|
+
handler: async ({ pathParams }, { json }) => {
|
|
124
|
+
expectTypeOf<typeof pathParams>().toEqualTypeOf<{ id: string }>();
|
|
125
|
+
return json({ id: Number(pathParams.id), name: "John" });
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
expect(route.method).toBe("GET");
|
|
130
|
+
expect(route.path).toBe("/users/:id");
|
|
131
|
+
expect(route.outputSchema).toBeDefined();
|
|
132
|
+
expect(route.handler).toBeDefined();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("defineRoute with multiple path parameters", () => {
|
|
136
|
+
const route = defineRoute({
|
|
137
|
+
method: "GET",
|
|
138
|
+
path: "/organizations/:orgId/users/:userId",
|
|
139
|
+
outputSchema: z.object({ orgId: z.number(), userId: z.number() }),
|
|
140
|
+
handler: async ({ pathParams }, { json }) => {
|
|
141
|
+
expectTypeOf<typeof pathParams>().toEqualTypeOf<{ orgId: string; userId: string }>();
|
|
142
|
+
return json({ orgId: Number(pathParams.orgId), userId: Number(pathParams.userId) });
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
expect(route.method).toBe("GET");
|
|
147
|
+
expect(route.path).toBe("/organizations/:orgId/users/:userId");
|
|
148
|
+
expect(route.outputSchema).toBeDefined();
|
|
149
|
+
expect(route.handler).toBeDefined();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("defineRoute returns the same config object", () => {
|
|
153
|
+
const config = {
|
|
154
|
+
method: "GET" as const,
|
|
155
|
+
path: "/test" as const,
|
|
156
|
+
handler: async (_ctx: unknown, { empty }: { empty: () => Response }) => empty(),
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const route = defineRoute(config);
|
|
160
|
+
expect(route).toBe(config);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("defineRoute with ValidPath type checking", () => {
|
|
164
|
+
// Valid path
|
|
165
|
+
const validRoute = defineRoute({
|
|
166
|
+
method: "GET",
|
|
167
|
+
path: "/api/users",
|
|
168
|
+
handler: async (_ctx, { empty }) => empty(),
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
expectTypeOf(validRoute.path).toEqualTypeOf<"/api/users">();
|
|
172
|
+
|
|
173
|
+
// TODO: Once ValidPath is integrated with defineRoute, add tests for invalid paths
|
|
174
|
+
// Currently defineRoute doesn't enforce ValidPath constraints
|
|
175
|
+
});
|
|
176
|
+
});
|
package/src/api/route.ts
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import type { StandardSchemaV1 } from "@standard-schema/spec";
|
|
2
|
+
import type { FragnoRouteConfig, HTTPMethod } from "./api";
|
|
3
|
+
|
|
4
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
5
|
+
export type AnyFragnoRouteConfig = FragnoRouteConfig<HTTPMethod, string, any, any, any, any>;
|
|
6
|
+
|
|
7
|
+
export interface RouteFactoryContext<TConfig, TDeps, TServices> {
|
|
8
|
+
config: TConfig;
|
|
9
|
+
deps: TDeps;
|
|
10
|
+
services: TServices;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type RouteFactory<
|
|
14
|
+
TConfig,
|
|
15
|
+
TDeps,
|
|
16
|
+
TServices,
|
|
17
|
+
TRoutes extends readonly FragnoRouteConfig<
|
|
18
|
+
HTTPMethod,
|
|
19
|
+
string,
|
|
20
|
+
StandardSchemaV1 | undefined,
|
|
21
|
+
StandardSchemaV1 | undefined,
|
|
22
|
+
string,
|
|
23
|
+
string
|
|
24
|
+
>[],
|
|
25
|
+
> = (context: RouteFactoryContext<TConfig, TDeps, TServices>) => TRoutes;
|
|
26
|
+
|
|
27
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
28
|
+
export type AnyRouteOrFactory = AnyFragnoRouteConfig | RouteFactory<any, any, any, any>;
|
|
29
|
+
|
|
30
|
+
export type FlattenRouteFactories<T extends readonly AnyRouteOrFactory[]> = T extends readonly [
|
|
31
|
+
infer First,
|
|
32
|
+
...infer Rest extends readonly AnyRouteOrFactory[],
|
|
33
|
+
]
|
|
34
|
+
? // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
35
|
+
First extends RouteFactory<any, any, any, infer TRoutes>
|
|
36
|
+
? [...TRoutes, ...FlattenRouteFactories<Rest>]
|
|
37
|
+
: [First, ...FlattenRouteFactories<Rest>]
|
|
38
|
+
: [];
|
|
39
|
+
|
|
40
|
+
// Helper to resolve route factories into routes
|
|
41
|
+
export function resolveRouteFactories<
|
|
42
|
+
TConfig,
|
|
43
|
+
TDeps,
|
|
44
|
+
TServices,
|
|
45
|
+
const TRoutesOrFactories extends readonly AnyRouteOrFactory[],
|
|
46
|
+
>(
|
|
47
|
+
context: RouteFactoryContext<TConfig, TDeps, TServices>,
|
|
48
|
+
routesOrFactories: TRoutesOrFactories,
|
|
49
|
+
): FlattenRouteFactories<TRoutesOrFactories> {
|
|
50
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
51
|
+
const routes: any[] = [];
|
|
52
|
+
|
|
53
|
+
for (const item of routesOrFactories) {
|
|
54
|
+
if (typeof item === "function") {
|
|
55
|
+
// It's a route factory
|
|
56
|
+
const factoryRoutes = item(context);
|
|
57
|
+
routes.push(...factoryRoutes);
|
|
58
|
+
} else {
|
|
59
|
+
// It's a direct route
|
|
60
|
+
routes.push(item);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return routes as FlattenRouteFactories<TRoutesOrFactories>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// TODO(Wilco): Do these overloads actually do anything?
|
|
68
|
+
// TODO(Wilco): ValidPath<T> should be added back in here.
|
|
69
|
+
|
|
70
|
+
// Overload for routes without inputSchema
|
|
71
|
+
export function defineRoute<
|
|
72
|
+
const TMethod extends HTTPMethod,
|
|
73
|
+
const TPath extends string,
|
|
74
|
+
const TOutputSchema extends StandardSchemaV1 | undefined,
|
|
75
|
+
const TErrorCode extends string = string,
|
|
76
|
+
const TQueryParameters extends string = string,
|
|
77
|
+
>(
|
|
78
|
+
config: FragnoRouteConfig<
|
|
79
|
+
TMethod,
|
|
80
|
+
TPath,
|
|
81
|
+
undefined,
|
|
82
|
+
TOutputSchema,
|
|
83
|
+
TErrorCode,
|
|
84
|
+
TQueryParameters
|
|
85
|
+
> & { inputSchema?: undefined },
|
|
86
|
+
): FragnoRouteConfig<TMethod, TPath, undefined, TOutputSchema, TErrorCode, TQueryParameters>;
|
|
87
|
+
|
|
88
|
+
// Overload for routes with inputSchema
|
|
89
|
+
export function defineRoute<
|
|
90
|
+
const TMethod extends HTTPMethod,
|
|
91
|
+
const TPath extends string,
|
|
92
|
+
const TInputSchema extends StandardSchemaV1,
|
|
93
|
+
const TOutputSchema extends StandardSchemaV1 | undefined,
|
|
94
|
+
const TErrorCode extends string = string,
|
|
95
|
+
const TQueryParameters extends string = string,
|
|
96
|
+
>(
|
|
97
|
+
config: FragnoRouteConfig<
|
|
98
|
+
TMethod,
|
|
99
|
+
TPath,
|
|
100
|
+
TInputSchema,
|
|
101
|
+
TOutputSchema,
|
|
102
|
+
TErrorCode,
|
|
103
|
+
TQueryParameters
|
|
104
|
+
> & { inputSchema: TInputSchema },
|
|
105
|
+
): FragnoRouteConfig<TMethod, TPath, TInputSchema, TOutputSchema, TErrorCode, TQueryParameters>;
|
|
106
|
+
|
|
107
|
+
// implementation
|
|
108
|
+
export function defineRoute<
|
|
109
|
+
const TMethod extends HTTPMethod,
|
|
110
|
+
const TPath extends string,
|
|
111
|
+
const TInputSchema extends StandardSchemaV1 | undefined,
|
|
112
|
+
const TOutputSchema extends StandardSchemaV1 | undefined,
|
|
113
|
+
const TErrorCode extends string = string,
|
|
114
|
+
const TQueryParameters extends string = string,
|
|
115
|
+
>(
|
|
116
|
+
config: FragnoRouteConfig<
|
|
117
|
+
TMethod,
|
|
118
|
+
TPath,
|
|
119
|
+
TInputSchema,
|
|
120
|
+
TOutputSchema,
|
|
121
|
+
TErrorCode,
|
|
122
|
+
TQueryParameters
|
|
123
|
+
>,
|
|
124
|
+
): FragnoRouteConfig<TMethod, TPath, TInputSchema, TOutputSchema, TErrorCode, TQueryParameters> {
|
|
125
|
+
return config;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
129
|
+
export type EmptyObject = {}; //Record<string, never>;
|
|
130
|
+
|
|
131
|
+
export function defineRoutes<
|
|
132
|
+
TConfig = EmptyObject,
|
|
133
|
+
TDeps = EmptyObject,
|
|
134
|
+
TServices = EmptyObject,
|
|
135
|
+
>() {
|
|
136
|
+
return {
|
|
137
|
+
create: <
|
|
138
|
+
const TRoutes extends readonly FragnoRouteConfig<
|
|
139
|
+
HTTPMethod,
|
|
140
|
+
string,
|
|
141
|
+
StandardSchemaV1 | undefined,
|
|
142
|
+
StandardSchemaV1 | undefined,
|
|
143
|
+
string,
|
|
144
|
+
string
|
|
145
|
+
>[],
|
|
146
|
+
>(
|
|
147
|
+
fn: (context: RouteFactoryContext<TConfig, TDeps, TServices>) => TRoutes,
|
|
148
|
+
): RouteFactory<TConfig, TDeps, TServices, TRoutes> => {
|
|
149
|
+
return fn;
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
}
|