rouzer 1.0.0-beta.1 → 1.0.0-beta.10
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/dist/client/index.d.ts +36 -0
- package/dist/client/index.js +55 -0
- package/dist/common.d.ts +47 -0
- package/dist/common.js +51 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +4 -0
- package/dist/route.d.ts +7 -0
- package/dist/route.js +18 -0
- package/dist/server/router.d.ts +111 -0
- package/dist/server/router.js +175 -0
- package/dist/types.d.ts +85 -0
- package/dist/types.js +1 -0
- package/package.json +18 -6
- package/readme.md +1 -1
- package/.vscode/settings.json +0 -5
- package/src/client/index.ts +0 -70
- package/src/common.ts +0 -99
- package/src/index.ts +0 -4
- package/src/route.ts +0 -44
- package/src/server/router.ts +0 -248
- package/src/types.ts +0 -89
- package/tsconfig.json +0 -10
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Promisable, RouteRequest } from '../types.js';
|
|
2
|
+
export declare function createClient(config: {
|
|
3
|
+
/**
|
|
4
|
+
* Base URL to use for all requests.
|
|
5
|
+
*/
|
|
6
|
+
baseURL: string;
|
|
7
|
+
/**
|
|
8
|
+
* Default headers to send with every request.
|
|
9
|
+
*/
|
|
10
|
+
headers?: Record<string, string>;
|
|
11
|
+
/**
|
|
12
|
+
* Custom handler for non-200 response to a `.json()` request. By default, the
|
|
13
|
+
* response is always parsed as JSON, regardless of the HTTP status code.
|
|
14
|
+
*/
|
|
15
|
+
onJsonError?: (response: Response) => Promisable<Response>;
|
|
16
|
+
}): {
|
|
17
|
+
config: {
|
|
18
|
+
/**
|
|
19
|
+
* Base URL to use for all requests.
|
|
20
|
+
*/
|
|
21
|
+
baseURL: string;
|
|
22
|
+
/**
|
|
23
|
+
* Default headers to send with every request.
|
|
24
|
+
*/
|
|
25
|
+
headers?: Record<string, string> | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* Custom handler for non-200 response to a `.json()` request. By default, the
|
|
28
|
+
* response is always parsed as JSON, regardless of the HTTP status code.
|
|
29
|
+
*/
|
|
30
|
+
onJsonError?: ((response: Response) => Promisable<Response>) | undefined;
|
|
31
|
+
};
|
|
32
|
+
request<T extends RouteRequest>({ path: pathBuilder, method, args: { path, query, body, headers }, route, }: T): Promise<Response & {
|
|
33
|
+
json(): Promise<T["$result"]>;
|
|
34
|
+
}>;
|
|
35
|
+
json<T extends RouteRequest>(request: T): Promise<T["$result"]>;
|
|
36
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { shake } from '../common.js';
|
|
2
|
+
export function createClient(config) {
|
|
3
|
+
const baseURL = config.baseURL.replace(/\/$/, '');
|
|
4
|
+
return {
|
|
5
|
+
config,
|
|
6
|
+
request({ path: pathBuilder, method, args: { path, query, body, headers }, route, }) {
|
|
7
|
+
if (route.path) {
|
|
8
|
+
path = route.path.parse(path);
|
|
9
|
+
}
|
|
10
|
+
let url;
|
|
11
|
+
const href = pathBuilder.href(path);
|
|
12
|
+
if (href[0] === '/') {
|
|
13
|
+
url = new URL(baseURL);
|
|
14
|
+
url.pathname += pathBuilder.href(path);
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
url = new URL(href);
|
|
18
|
+
}
|
|
19
|
+
if (route.query) {
|
|
20
|
+
query = route.query.parse(query ?? {});
|
|
21
|
+
url.search = new URLSearchParams(query).toString();
|
|
22
|
+
}
|
|
23
|
+
else if (query) {
|
|
24
|
+
throw new Error('Unexpected query parameters');
|
|
25
|
+
}
|
|
26
|
+
if (route.body) {
|
|
27
|
+
body = route.body.parse(body !== undefined ? body : {});
|
|
28
|
+
}
|
|
29
|
+
else if (body !== undefined) {
|
|
30
|
+
throw new Error('Unexpected body');
|
|
31
|
+
}
|
|
32
|
+
if (config.headers || headers) {
|
|
33
|
+
headers = {
|
|
34
|
+
...config.headers,
|
|
35
|
+
...(headers && shake(headers)),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
if (route.headers) {
|
|
39
|
+
headers = route.headers.parse(headers);
|
|
40
|
+
}
|
|
41
|
+
return fetch(url, {
|
|
42
|
+
method,
|
|
43
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
44
|
+
headers: headers,
|
|
45
|
+
});
|
|
46
|
+
},
|
|
47
|
+
async json(request) {
|
|
48
|
+
const response = await this.request(request);
|
|
49
|
+
if (!response.ok && config.onJsonError) {
|
|
50
|
+
return config.onJsonError(response);
|
|
51
|
+
}
|
|
52
|
+
return response.json();
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
package/dist/common.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Map over all the keys to create a new object.
|
|
3
|
+
*
|
|
4
|
+
* @see https://radashi.js.org/reference/object/mapEntries
|
|
5
|
+
* @example
|
|
6
|
+
* ```ts
|
|
7
|
+
* const a = { a: 1, b: 2, c: 3 }
|
|
8
|
+
* mapEntries(a, (key, value) => [value, key])
|
|
9
|
+
* // => { 1: 'a', 2: 'b', 3: 'c' }
|
|
10
|
+
* ```
|
|
11
|
+
* @version 12.1.0
|
|
12
|
+
*/
|
|
13
|
+
export declare function mapEntries<TKey extends string | number | symbol, TValue, TNewKey extends string | number | symbol, TNewValue>(obj: Record<TKey, TValue>, toEntry: (key: TKey, value: TValue) => [TNewKey, TNewValue]): Record<TNewKey, TNewValue>;
|
|
14
|
+
/**
|
|
15
|
+
* Removes (shakes out) undefined entries from an object. Optional
|
|
16
|
+
* second argument shakes out values by custom evaluation.
|
|
17
|
+
*
|
|
18
|
+
* Note that non-enumerable keys are never shaken out.
|
|
19
|
+
*
|
|
20
|
+
* @see https://radashi.js.org/reference/object/shake
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts
|
|
23
|
+
* const a = { a: 1, b: undefined, c: 3 }
|
|
24
|
+
* shake(a)
|
|
25
|
+
* // => { a: 1, c: 3 }
|
|
26
|
+
* ```
|
|
27
|
+
* @version 12.1.0
|
|
28
|
+
*/
|
|
29
|
+
export declare function shake<T extends object>(obj: T): {
|
|
30
|
+
[K in keyof T]: Exclude<T[K], undefined>;
|
|
31
|
+
};
|
|
32
|
+
export declare function shake<T extends object>(obj: T, filter: ((value: unknown) => boolean) | undefined): T;
|
|
33
|
+
/**
|
|
34
|
+
* Map over all the keys to create a new object.
|
|
35
|
+
*
|
|
36
|
+
* @see https://radashi.js.org/reference/object/mapValues
|
|
37
|
+
* @example
|
|
38
|
+
* ```ts
|
|
39
|
+
* const a = { a: 1, b: 2, c: 3 }
|
|
40
|
+
* mapValues(a, (value, key) => value * 2)
|
|
41
|
+
* // => { a: 2, b: 4, c: 6 }
|
|
42
|
+
* ```
|
|
43
|
+
* @version 12.1.0
|
|
44
|
+
*/
|
|
45
|
+
export declare function mapValues<T extends object, U>(obj: T, mapFunc: (value: Required<T>[keyof T], key: keyof T) => U): {
|
|
46
|
+
[K in keyof T]: U;
|
|
47
|
+
};
|
package/dist/common.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Map over all the keys to create a new object.
|
|
3
|
+
*
|
|
4
|
+
* @see https://radashi.js.org/reference/object/mapEntries
|
|
5
|
+
* @example
|
|
6
|
+
* ```ts
|
|
7
|
+
* const a = { a: 1, b: 2, c: 3 }
|
|
8
|
+
* mapEntries(a, (key, value) => [value, key])
|
|
9
|
+
* // => { 1: 'a', 2: 'b', 3: 'c' }
|
|
10
|
+
* ```
|
|
11
|
+
* @version 12.1.0
|
|
12
|
+
*/
|
|
13
|
+
export function mapEntries(obj, toEntry) {
|
|
14
|
+
if (!obj) {
|
|
15
|
+
return {};
|
|
16
|
+
}
|
|
17
|
+
return Object.entries(obj).reduce((acc, [key, value]) => {
|
|
18
|
+
const [newKey, newValue] = toEntry(key, value);
|
|
19
|
+
acc[newKey] = newValue;
|
|
20
|
+
return acc;
|
|
21
|
+
}, {});
|
|
22
|
+
}
|
|
23
|
+
export function shake(obj, filter = value => value === undefined) {
|
|
24
|
+
if (!obj) {
|
|
25
|
+
return {};
|
|
26
|
+
}
|
|
27
|
+
return Object.keys(obj).reduce((acc, key) => {
|
|
28
|
+
if (!filter(obj[key])) {
|
|
29
|
+
acc[key] = obj[key];
|
|
30
|
+
}
|
|
31
|
+
return acc;
|
|
32
|
+
}, {});
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Map over all the keys to create a new object.
|
|
36
|
+
*
|
|
37
|
+
* @see https://radashi.js.org/reference/object/mapValues
|
|
38
|
+
* @example
|
|
39
|
+
* ```ts
|
|
40
|
+
* const a = { a: 1, b: 2, c: 3 }
|
|
41
|
+
* mapValues(a, (value, key) => value * 2)
|
|
42
|
+
* // => { a: 2, b: 4, c: 6 }
|
|
43
|
+
* ```
|
|
44
|
+
* @version 12.1.0
|
|
45
|
+
*/
|
|
46
|
+
export function mapValues(obj, mapFunc) {
|
|
47
|
+
return Object.keys(obj).reduce((acc, key) => {
|
|
48
|
+
acc[key] = mapFunc(obj[key], key);
|
|
49
|
+
return acc;
|
|
50
|
+
}, {});
|
|
51
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
package/dist/route.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { RoutePattern } from '@remix-run/route-pattern';
|
|
2
|
+
import type { MutationMethod, QueryMethod, RouteFunction, RouteMethods, Unchecked } from './types.js';
|
|
3
|
+
export declare function $type<T>(): Unchecked<T>;
|
|
4
|
+
export declare function route<P extends string, T extends RouteMethods>(pattern: P, methods: T): {
|
|
5
|
+
path: RoutePattern<P>;
|
|
6
|
+
methods: T;
|
|
7
|
+
} & { [K in keyof T]: RouteFunction<Extract<T[K], MutationMethod | QueryMethod>>; };
|
package/dist/route.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { RoutePattern } from '@remix-run/route-pattern';
|
|
2
|
+
import { mapEntries } from './common.js';
|
|
3
|
+
export function $type() {
|
|
4
|
+
return null;
|
|
5
|
+
}
|
|
6
|
+
export function route(pattern, methods) {
|
|
7
|
+
const path = new RoutePattern(pattern);
|
|
8
|
+
const createFetch = (method, route) => (args) => {
|
|
9
|
+
return {
|
|
10
|
+
route,
|
|
11
|
+
path,
|
|
12
|
+
method,
|
|
13
|
+
args,
|
|
14
|
+
$result: undefined,
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
return Object.assign({ path, methods }, mapEntries(methods, (method, route) => [method, createFetch(method, route)]));
|
|
18
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type { AdapterRequestContext } from '@hattip/core';
|
|
2
|
+
import { type Params } from '@remix-run/route-pattern';
|
|
3
|
+
import { chain, MiddlewareChain, type MiddlewareContext } from 'alien-middleware';
|
|
4
|
+
import * as z from 'zod/mini';
|
|
5
|
+
import type { InferRouteResponse, MutationMethod, Promisable, QueryMethod, Routes } from '../types.js';
|
|
6
|
+
export { chain };
|
|
7
|
+
type EmptyMiddlewareChain<TPlatform = unknown> = MiddlewareChain<{
|
|
8
|
+
initial: {
|
|
9
|
+
env: {};
|
|
10
|
+
properties: {};
|
|
11
|
+
};
|
|
12
|
+
current: {
|
|
13
|
+
env: {};
|
|
14
|
+
properties: {};
|
|
15
|
+
};
|
|
16
|
+
platform: TPlatform;
|
|
17
|
+
}>;
|
|
18
|
+
export type RouterConfig = {
|
|
19
|
+
/**
|
|
20
|
+
* Base path to prepend to all routes.
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts
|
|
23
|
+
* basePath: 'api/',
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
basePath?: string;
|
|
27
|
+
/**
|
|
28
|
+
* Routes to match.
|
|
29
|
+
* @example
|
|
30
|
+
* ```ts
|
|
31
|
+
* // This namespace contains your `route()` declarations.
|
|
32
|
+
* // Pass it to the `createRouter` function.
|
|
33
|
+
* import * as routes from './routes'
|
|
34
|
+
*
|
|
35
|
+
* createRouter({ routes })({
|
|
36
|
+
* // your route handlers...
|
|
37
|
+
* })
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
routes: Routes;
|
|
41
|
+
/**
|
|
42
|
+
* Middleware to apply to all routes.
|
|
43
|
+
* @see https://github.com/alien-rpc/alien-middleware#quick-start
|
|
44
|
+
* @example
|
|
45
|
+
* ```ts
|
|
46
|
+
* middlewares: chain().use(ctx => {
|
|
47
|
+
* return {
|
|
48
|
+
* db: postgres(ctx.env('POSTGRES_URL')),
|
|
49
|
+
* }
|
|
50
|
+
* }),
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
middlewares?: MiddlewareChain;
|
|
54
|
+
/**
|
|
55
|
+
* Enable debugging features.
|
|
56
|
+
* - When a handler throws an error, include its message in the response body.
|
|
57
|
+
* - Throw an error if a handler is not found for a route.
|
|
58
|
+
* @example
|
|
59
|
+
* ```ts
|
|
60
|
+
* debug: process.env.NODE_ENV !== 'production',
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
debug?: boolean;
|
|
64
|
+
/**
|
|
65
|
+
* CORS configuration.
|
|
66
|
+
*/
|
|
67
|
+
cors?: {
|
|
68
|
+
/**
|
|
69
|
+
* If defined, requests must have an `Origin` header that is in this list.
|
|
70
|
+
*
|
|
71
|
+
* Origins may contain wildcards for protocol and subdomain. The protocol is
|
|
72
|
+
* optional and defaults to `https`.
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* ```ts
|
|
76
|
+
* allowOrigins: ['example.net', 'https://*.example.com', '*://localhost:3000']
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
allowOrigins?: string[];
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
interface CreateRouterConfig<TRoutes extends Routes, TMiddleware extends MiddlewareChain> extends RouterConfig {
|
|
83
|
+
routes: TRoutes;
|
|
84
|
+
middlewares?: TMiddleware;
|
|
85
|
+
}
|
|
86
|
+
export declare function createRouter<TRoutes extends Routes, TMiddleware extends MiddlewareChain = EmptyMiddlewareChain>(config: CreateRouterConfig<TRoutes, TMiddleware>): (handlers: { [K in keyof TRoutes]: { [M in keyof TRoutes[K]["methods"]]: TRoutes[K]["methods"][M] extends infer T ? T extends TRoutes[K]["methods"][M] ? T extends QueryMethod ? (context: MiddlewareContext<TMiddleware> & {
|
|
87
|
+
path: T extends {
|
|
88
|
+
path: any;
|
|
89
|
+
} ? z.infer<T["path"]> : Params<TRoutes[K]["path"]["source"]>;
|
|
90
|
+
query: z.infer<T["query"]>;
|
|
91
|
+
headers: z.infer<T["headers"]>;
|
|
92
|
+
}) => Promisable<Response | InferRouteResponse<T>> : T extends MutationMethod ? (context: MiddlewareContext<TMiddleware> & {
|
|
93
|
+
path: T extends {
|
|
94
|
+
path: any;
|
|
95
|
+
} ? z.infer<T["path"]> : Params<TRoutes[K]["path"]["source"]>;
|
|
96
|
+
body: z.infer<T["body"]>;
|
|
97
|
+
headers: z.infer<T["headers"]>;
|
|
98
|
+
}) => Promisable<Response | InferRouteResponse<T>> : never : never : never; }; }) => import("alien-middleware").ApplyMiddleware<TMiddleware, (context: AdapterRequestContext<TMiddleware extends MiddlewareChain<infer T extends {
|
|
99
|
+
initial: {
|
|
100
|
+
env: object;
|
|
101
|
+
properties: object;
|
|
102
|
+
};
|
|
103
|
+
current: {
|
|
104
|
+
env: object;
|
|
105
|
+
properties: object;
|
|
106
|
+
};
|
|
107
|
+
platform: unknown;
|
|
108
|
+
}> ? T["platform"] : never> & {
|
|
109
|
+
url?: URL | undefined;
|
|
110
|
+
path?: {} | undefined;
|
|
111
|
+
}) => Promise<Response | undefined>>;
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { RoutePattern } from '@remix-run/route-pattern';
|
|
2
|
+
import { chain, } from 'alien-middleware';
|
|
3
|
+
import { mapValues } from '../common.js';
|
|
4
|
+
import * as z from 'zod/mini';
|
|
5
|
+
export { chain };
|
|
6
|
+
export function createRouter(config) {
|
|
7
|
+
const keys = Object.keys(config.routes);
|
|
8
|
+
const middlewares = config.middlewares ?? chain();
|
|
9
|
+
const basePath = config.basePath?.replace(/\/?$/, '/');
|
|
10
|
+
const patterns = mapValues(config.routes, ({ path }) => basePath ? new RoutePattern(path.source.replace(/^\/?/, basePath)) : path);
|
|
11
|
+
const allowOrigins = config.cors?.allowOrigins?.map(origin => {
|
|
12
|
+
if (!origin.includes('//')) {
|
|
13
|
+
origin = `https://${origin}`;
|
|
14
|
+
}
|
|
15
|
+
if (origin.includes('*')) {
|
|
16
|
+
return new RegExp(`^${origin
|
|
17
|
+
.replace(/\./g, '\\.')
|
|
18
|
+
.replace(/\*:/g, '[^:]+:') // Wildcard protocol
|
|
19
|
+
.replace(/\*\./g, '([^/.]+\\.)?') // Wildcard subdomain
|
|
20
|
+
}$`);
|
|
21
|
+
}
|
|
22
|
+
return new ExactPattern(origin);
|
|
23
|
+
});
|
|
24
|
+
return (handlers) => middlewares.use(async function (context) {
|
|
25
|
+
const request = context.request;
|
|
26
|
+
const url = (context.url ??= new URL(request.url));
|
|
27
|
+
let method = request.method.toUpperCase();
|
|
28
|
+
let isPreflight = false;
|
|
29
|
+
if (method === 'OPTIONS') {
|
|
30
|
+
method = request.headers.get('Access-Control-Request-Method') ?? 'GET';
|
|
31
|
+
isPreflight = true;
|
|
32
|
+
}
|
|
33
|
+
for (let i = 0; i < keys.length; i++) {
|
|
34
|
+
const route = config.routes[keys[i]].methods[method];
|
|
35
|
+
if (!route) {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
const match = patterns[keys[i]].match(url);
|
|
39
|
+
if (!match) {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
const handler = handlers[keys[i]][method];
|
|
43
|
+
if (!handler) {
|
|
44
|
+
if (config.debug) {
|
|
45
|
+
throw new Error(`Handler not found for route: ${keys[i]} ${method}`);
|
|
46
|
+
}
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (isPreflight) {
|
|
50
|
+
const origin = request.headers.get('Origin');
|
|
51
|
+
if (allowOrigins &&
|
|
52
|
+
!(origin && allowOrigins.some(pattern => pattern.test(origin)))) {
|
|
53
|
+
return new Response(null, { status: 403 });
|
|
54
|
+
}
|
|
55
|
+
return new Response(null, {
|
|
56
|
+
headers: {
|
|
57
|
+
'Access-Control-Allow-Origin': origin ?? '*',
|
|
58
|
+
'Access-Control-Allow-Methods': method,
|
|
59
|
+
'Access-Control-Allow-Headers': request.headers.get('Access-Control-Request-Headers') ?? '',
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
if (route.path) {
|
|
64
|
+
const error = parsePathParams(context, enableStringParsing(route.path), match.params);
|
|
65
|
+
if (error) {
|
|
66
|
+
return httpClientError(error, 'Invalid path parameter', config);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
context.path = match.params;
|
|
71
|
+
}
|
|
72
|
+
if (route.headers) {
|
|
73
|
+
const error = parseHeaders(context, enableStringParsing(route.headers));
|
|
74
|
+
if (error) {
|
|
75
|
+
return httpClientError(error, 'Invalid request headers', config);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (route.query) {
|
|
79
|
+
const error = parseQueryString(context, enableStringParsing(route.query));
|
|
80
|
+
if (error) {
|
|
81
|
+
return httpClientError(error, 'Invalid query string', config);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (route.body) {
|
|
85
|
+
const error = await parseRequestBody(context, route.body);
|
|
86
|
+
if (error) {
|
|
87
|
+
return httpClientError(error, 'Invalid request body', config);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const result = await handler(context);
|
|
91
|
+
if (result instanceof Response) {
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
return Response.json(result);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
function httpClientError(error, message, config) {
|
|
99
|
+
return Response.json({
|
|
100
|
+
...error,
|
|
101
|
+
message: config.debug ? `${message}: ${error.message}` : message,
|
|
102
|
+
}, { status: 400 });
|
|
103
|
+
}
|
|
104
|
+
function parsePathParams(context, schema, params) {
|
|
105
|
+
const result = schema.safeParse(params);
|
|
106
|
+
if (!result.success) {
|
|
107
|
+
return result.error;
|
|
108
|
+
}
|
|
109
|
+
context.path = result.data;
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
function parseHeaders(context, schema) {
|
|
113
|
+
const headers = Object.fromEntries(context.request.headers);
|
|
114
|
+
const result = schema.safeParse(headers);
|
|
115
|
+
if (!result.success) {
|
|
116
|
+
return result.error;
|
|
117
|
+
}
|
|
118
|
+
context.headers = result.data;
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
function parseQueryString(context, schema) {
|
|
122
|
+
const result = schema.safeParse(Object.fromEntries(context.url.searchParams));
|
|
123
|
+
if (!result.success) {
|
|
124
|
+
return result.error;
|
|
125
|
+
}
|
|
126
|
+
context.query = result.data;
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
async function parseRequestBody(context, schema) {
|
|
130
|
+
const result = await context.request.json().then(body => schema.safeParse(body), error => ({ success: false, error }));
|
|
131
|
+
if (!result.success) {
|
|
132
|
+
return result.error;
|
|
133
|
+
}
|
|
134
|
+
context.body = result.data;
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
const seen = new WeakMap();
|
|
138
|
+
/**
|
|
139
|
+
* Traverse object and array schemas, finding schemas that expect a number or
|
|
140
|
+
* boolean, and replace those schemas with a new schema that parses the input
|
|
141
|
+
* value as a number or boolean.
|
|
142
|
+
*/
|
|
143
|
+
function enableStringParsing(schema) {
|
|
144
|
+
if (schema.type === 'number') {
|
|
145
|
+
return z.pipe(z.transform(Number), schema);
|
|
146
|
+
}
|
|
147
|
+
if (schema.type === 'boolean') {
|
|
148
|
+
return z.pipe(z.transform(toBooleanStrict), schema);
|
|
149
|
+
}
|
|
150
|
+
if (schema.type === 'object') {
|
|
151
|
+
const cached = seen.get(schema);
|
|
152
|
+
if (cached) {
|
|
153
|
+
return cached;
|
|
154
|
+
}
|
|
155
|
+
const modified = z.object(mapValues(schema.def.shape, enableStringParsing));
|
|
156
|
+
seen.set(schema, modified);
|
|
157
|
+
return modified;
|
|
158
|
+
}
|
|
159
|
+
if (schema.type === 'array') {
|
|
160
|
+
return z.array(enableStringParsing(schema.def.element));
|
|
161
|
+
}
|
|
162
|
+
return schema;
|
|
163
|
+
}
|
|
164
|
+
function toBooleanStrict(value) {
|
|
165
|
+
return value === 'true' || (value === 'false' ? false : value);
|
|
166
|
+
}
|
|
167
|
+
class ExactPattern {
|
|
168
|
+
value;
|
|
169
|
+
constructor(value) {
|
|
170
|
+
this.value = value;
|
|
171
|
+
}
|
|
172
|
+
test(input) {
|
|
173
|
+
return input === this.value;
|
|
174
|
+
}
|
|
175
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { Params, RoutePattern } from '@remix-run/route-pattern';
|
|
2
|
+
import * as z from 'zod/mini';
|
|
3
|
+
export type Promisable<T> = T | Promise<T>;
|
|
4
|
+
export type Unchecked<T> = {
|
|
5
|
+
__unchecked__: T;
|
|
6
|
+
};
|
|
7
|
+
export type QueryMethod = {
|
|
8
|
+
path?: z.ZodMiniObject<any>;
|
|
9
|
+
query?: z.ZodMiniObject<any>;
|
|
10
|
+
body?: never;
|
|
11
|
+
headers?: z.ZodMiniObject<any>;
|
|
12
|
+
response: Unchecked<any>;
|
|
13
|
+
};
|
|
14
|
+
export type MutationMethod = {
|
|
15
|
+
path?: z.ZodMiniObject<any>;
|
|
16
|
+
query?: never;
|
|
17
|
+
body: z.ZodMiniType<any, any>;
|
|
18
|
+
headers?: z.ZodMiniObject<any>;
|
|
19
|
+
response?: Unchecked<any>;
|
|
20
|
+
};
|
|
21
|
+
export type RouteMethods = {
|
|
22
|
+
GET?: QueryMethod;
|
|
23
|
+
POST?: MutationMethod;
|
|
24
|
+
PUT?: MutationMethod;
|
|
25
|
+
PATCH?: MutationMethod;
|
|
26
|
+
DELETE?: MutationMethod;
|
|
27
|
+
};
|
|
28
|
+
export type Routes = {
|
|
29
|
+
[key: string]: {
|
|
30
|
+
path: RoutePattern;
|
|
31
|
+
methods: RouteMethods;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
declare class Any {
|
|
35
|
+
private isAny;
|
|
36
|
+
}
|
|
37
|
+
type PathArgs<T> = T extends {
|
|
38
|
+
path: infer TPath extends string;
|
|
39
|
+
} ? Params<TPath> extends infer TParams ? {} extends TParams ? {
|
|
40
|
+
path?: TParams;
|
|
41
|
+
} : {
|
|
42
|
+
path: TParams;
|
|
43
|
+
} : unknown : unknown;
|
|
44
|
+
type QueryArgs<T> = T extends QueryMethod & {
|
|
45
|
+
query: infer TQuery;
|
|
46
|
+
} ? {} extends z.infer<TQuery> ? {
|
|
47
|
+
query?: z.infer<TQuery>;
|
|
48
|
+
} : {
|
|
49
|
+
query: z.infer<TQuery>;
|
|
50
|
+
} : unknown;
|
|
51
|
+
type MutationArgs<T> = T extends MutationMethod & {
|
|
52
|
+
body: infer TBody;
|
|
53
|
+
} ? {} extends z.infer<TBody> ? {
|
|
54
|
+
body?: z.infer<TBody>;
|
|
55
|
+
} : {
|
|
56
|
+
body: z.infer<TBody>;
|
|
57
|
+
} : unknown;
|
|
58
|
+
export type RouteArgs<T extends QueryMethod | MutationMethod = any> = ([
|
|
59
|
+
T
|
|
60
|
+
] extends [Any] ? {
|
|
61
|
+
query?: any;
|
|
62
|
+
body?: any;
|
|
63
|
+
path?: any;
|
|
64
|
+
} : QueryArgs<T> & MutationArgs<T> & PathArgs<T>) & Omit<RequestInit, 'method' | 'body' | 'headers'> & {
|
|
65
|
+
headers?: Record<string, string | undefined>;
|
|
66
|
+
};
|
|
67
|
+
export type RouteRequest<TResult = any> = {
|
|
68
|
+
route: QueryMethod | MutationMethod;
|
|
69
|
+
path: RoutePattern;
|
|
70
|
+
method: string;
|
|
71
|
+
args: RouteArgs;
|
|
72
|
+
$result: TResult;
|
|
73
|
+
};
|
|
74
|
+
export type RouteResponse<TResult = any> = Response & {
|
|
75
|
+
json(): Promise<TResult>;
|
|
76
|
+
};
|
|
77
|
+
export type InferRouteResponse<T extends QueryMethod | MutationMethod> = T extends {
|
|
78
|
+
response: Unchecked<infer TResponse>;
|
|
79
|
+
} ? TResponse : void;
|
|
80
|
+
export type RouteFunction<T extends QueryMethod | MutationMethod> = {
|
|
81
|
+
(args: RouteArgs<T>): RouteRequest<InferRouteResponse<T>>;
|
|
82
|
+
$args: RouteArgs<T>;
|
|
83
|
+
$response: InferRouteResponse<T>;
|
|
84
|
+
};
|
|
85
|
+
export {};
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rouzer",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.10",
|
|
4
|
+
"type": "module",
|
|
4
5
|
"exports": {
|
|
5
6
|
".": {
|
|
6
7
|
"types": "./dist/index.d.ts",
|
|
7
8
|
"import": "./dist/index.js"
|
|
8
9
|
}
|
|
9
10
|
},
|
|
10
|
-
"scripts": {
|
|
11
|
-
"build": "tsgo -b tsconfig.json"
|
|
12
|
-
},
|
|
13
11
|
"peerDependencies": {
|
|
14
12
|
"zod": ">=4"
|
|
15
13
|
},
|
|
@@ -17,6 +15,8 @@
|
|
|
17
15
|
"@alloc/prettier-config": "^1.0.0",
|
|
18
16
|
"@typescript/native-preview": "7.0.0-dev.20251208.1",
|
|
19
17
|
"prettier": "^3.7.4",
|
|
18
|
+
"tsc-lint": "^0.1.9",
|
|
19
|
+
"typescript": "^5.9.3",
|
|
20
20
|
"zod": "^4.1.13"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
@@ -24,5 +24,17 @@
|
|
|
24
24
|
"@remix-run/route-pattern": "^0.15.3",
|
|
25
25
|
"alien-middleware": "^0.10.2"
|
|
26
26
|
},
|
|
27
|
-
"prettier": "@alloc/prettier-config"
|
|
28
|
-
|
|
27
|
+
"prettier": "@alloc/prettier-config",
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "https://github.com/alloc/rouzer.git"
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"dist",
|
|
35
|
+
"!*.tsbuildinfo"
|
|
36
|
+
],
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "tsgo -b tsconfig.json"
|
|
39
|
+
}
|
|
40
|
+
}
|
package/readme.md
CHANGED
|
@@ -59,7 +59,7 @@ export const handler = createRouter({
|
|
|
59
59
|
})({
|
|
60
60
|
helloRoute: {
|
|
61
61
|
GET(ctx) {
|
|
62
|
-
const message = `Hello, ${ctx.
|
|
62
|
+
const message = `Hello, ${ctx.path.name}${ctx.query.excited ? '!' : '.'}`
|
|
63
63
|
return { message }
|
|
64
64
|
},
|
|
65
65
|
},
|
package/.vscode/settings.json
DELETED
package/src/client/index.ts
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import { shake } from '../common'
|
|
2
|
-
import type { RouteRequest } from '../types'
|
|
3
|
-
|
|
4
|
-
export function createClient(config: {
|
|
5
|
-
/**
|
|
6
|
-
* Base URL to use for all requests.
|
|
7
|
-
*/
|
|
8
|
-
baseURL: string
|
|
9
|
-
/**
|
|
10
|
-
* Default headers to send with every request.
|
|
11
|
-
*/
|
|
12
|
-
headers?: Record<string, string>
|
|
13
|
-
/**
|
|
14
|
-
* Custom handler for non-200 response to a `.json()` request. By default, the
|
|
15
|
-
* response is always parsed as JSON, regardless of the HTTP status code.
|
|
16
|
-
*/
|
|
17
|
-
onJsonError?: (response: Response) => Response
|
|
18
|
-
}) {
|
|
19
|
-
return {
|
|
20
|
-
config,
|
|
21
|
-
request<T extends RouteRequest>({
|
|
22
|
-
pathPattern,
|
|
23
|
-
method,
|
|
24
|
-
args: { path, query, body, headers },
|
|
25
|
-
route,
|
|
26
|
-
}: T) {
|
|
27
|
-
if (route.path) {
|
|
28
|
-
path = route.path.parse(path)
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const url = new URL(pathPattern.href(path), config.baseURL)
|
|
32
|
-
|
|
33
|
-
if (route.query) {
|
|
34
|
-
query = route.query.parse(query ?? {})
|
|
35
|
-
url.search = new URLSearchParams(query).toString()
|
|
36
|
-
} else if (query) {
|
|
37
|
-
throw new Error('Unexpected query parameters')
|
|
38
|
-
}
|
|
39
|
-
if (route.body) {
|
|
40
|
-
body = route.body.parse(body !== undefined ? body : {})
|
|
41
|
-
} else if (body !== undefined) {
|
|
42
|
-
throw new Error('Unexpected body')
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
if (config.headers || headers) {
|
|
46
|
-
headers = {
|
|
47
|
-
...config.headers,
|
|
48
|
-
...(headers && shake(headers)),
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
if (route.headers) {
|
|
53
|
-
headers = route.headers.parse(headers) as any
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
return fetch(url, {
|
|
57
|
-
method,
|
|
58
|
-
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
59
|
-
headers,
|
|
60
|
-
}) as Promise<Response & { json(): Promise<T['$result']> }>
|
|
61
|
-
},
|
|
62
|
-
async json<T extends RouteRequest>(request: T): Promise<T['$result']> {
|
|
63
|
-
const response = await this.request(request)
|
|
64
|
-
if (!response.ok && config.onJsonError) {
|
|
65
|
-
return config.onJsonError(response)
|
|
66
|
-
}
|
|
67
|
-
return response.json()
|
|
68
|
-
},
|
|
69
|
-
}
|
|
70
|
-
}
|
package/src/common.ts
DELETED
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Map over all the keys to create a new object.
|
|
3
|
-
*
|
|
4
|
-
* @see https://radashi.js.org/reference/object/mapEntries
|
|
5
|
-
* @example
|
|
6
|
-
* ```ts
|
|
7
|
-
* const a = { a: 1, b: 2, c: 3 }
|
|
8
|
-
* mapEntries(a, (key, value) => [value, key])
|
|
9
|
-
* // => { 1: 'a', 2: 'b', 3: 'c' }
|
|
10
|
-
* ```
|
|
11
|
-
* @version 12.1.0
|
|
12
|
-
*/
|
|
13
|
-
export function mapEntries<
|
|
14
|
-
TKey extends string | number | symbol,
|
|
15
|
-
TValue,
|
|
16
|
-
TNewKey extends string | number | symbol,
|
|
17
|
-
TNewValue,
|
|
18
|
-
>(
|
|
19
|
-
obj: Record<TKey, TValue>,
|
|
20
|
-
toEntry: (key: TKey, value: TValue) => [TNewKey, TNewValue]
|
|
21
|
-
): Record<TNewKey, TNewValue> {
|
|
22
|
-
if (!obj) {
|
|
23
|
-
return {} as Record<TNewKey, TNewValue>
|
|
24
|
-
}
|
|
25
|
-
return Object.entries(obj).reduce(
|
|
26
|
-
(acc, [key, value]) => {
|
|
27
|
-
const [newKey, newValue] = toEntry(key as TKey, value as TValue)
|
|
28
|
-
acc[newKey] = newValue
|
|
29
|
-
return acc
|
|
30
|
-
},
|
|
31
|
-
{} as Record<TNewKey, TNewValue>
|
|
32
|
-
)
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Removes (shakes out) undefined entries from an object. Optional
|
|
37
|
-
* second argument shakes out values by custom evaluation.
|
|
38
|
-
*
|
|
39
|
-
* Note that non-enumerable keys are never shaken out.
|
|
40
|
-
*
|
|
41
|
-
* @see https://radashi.js.org/reference/object/shake
|
|
42
|
-
* @example
|
|
43
|
-
* ```ts
|
|
44
|
-
* const a = { a: 1, b: undefined, c: 3 }
|
|
45
|
-
* shake(a)
|
|
46
|
-
* // => { a: 1, c: 3 }
|
|
47
|
-
* ```
|
|
48
|
-
* @version 12.1.0
|
|
49
|
-
*/
|
|
50
|
-
export function shake<T extends object>(
|
|
51
|
-
obj: T
|
|
52
|
-
): {
|
|
53
|
-
[K in keyof T]: Exclude<T[K], undefined>
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export function shake<T extends object>(
|
|
57
|
-
obj: T,
|
|
58
|
-
filter: ((value: unknown) => boolean) | undefined
|
|
59
|
-
): T
|
|
60
|
-
|
|
61
|
-
export function shake<T extends object>(
|
|
62
|
-
obj: T,
|
|
63
|
-
filter: (value: unknown) => boolean = value => value === undefined
|
|
64
|
-
): T {
|
|
65
|
-
if (!obj) {
|
|
66
|
-
return {} as T
|
|
67
|
-
}
|
|
68
|
-
return (Object.keys(obj) as (keyof T)[]).reduce((acc, key) => {
|
|
69
|
-
if (!filter(obj[key])) {
|
|
70
|
-
acc[key] = obj[key]
|
|
71
|
-
}
|
|
72
|
-
return acc
|
|
73
|
-
}, {} as T)
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Map over all the keys to create a new object.
|
|
78
|
-
*
|
|
79
|
-
* @see https://radashi.js.org/reference/object/mapValues
|
|
80
|
-
* @example
|
|
81
|
-
* ```ts
|
|
82
|
-
* const a = { a: 1, b: 2, c: 3 }
|
|
83
|
-
* mapValues(a, (value, key) => value * 2)
|
|
84
|
-
* // => { a: 2, b: 4, c: 6 }
|
|
85
|
-
* ```
|
|
86
|
-
* @version 12.1.0
|
|
87
|
-
*/
|
|
88
|
-
export function mapValues<T extends object, U>(
|
|
89
|
-
obj: T,
|
|
90
|
-
mapFunc: (value: Required<T>[keyof T], key: keyof T) => U
|
|
91
|
-
): { [K in keyof T]: U } {
|
|
92
|
-
return (Object.keys(obj) as (keyof T)[]).reduce(
|
|
93
|
-
(acc, key) => {
|
|
94
|
-
acc[key] = mapFunc(obj[key], key)
|
|
95
|
-
return acc
|
|
96
|
-
},
|
|
97
|
-
{} as { [K in keyof T]: U }
|
|
98
|
-
)
|
|
99
|
-
}
|
package/src/index.ts
DELETED
package/src/route.ts
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import { RoutePattern } from '@remix-run/route-pattern'
|
|
2
|
-
import { mapEntries } from './common'
|
|
3
|
-
import type {
|
|
4
|
-
MutationRoute,
|
|
5
|
-
QueryRoute,
|
|
6
|
-
RouteArgs,
|
|
7
|
-
RouteFunction,
|
|
8
|
-
RouteRequest,
|
|
9
|
-
Routes,
|
|
10
|
-
Unchecked,
|
|
11
|
-
} from './types'
|
|
12
|
-
|
|
13
|
-
export function $type<T>() {
|
|
14
|
-
return null as unknown as Unchecked<T>
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function route<P extends string, T extends Routes>(path: P, routes: T) {
|
|
18
|
-
const pathPattern = new RoutePattern(path)
|
|
19
|
-
const createFetch =
|
|
20
|
-
(method: string, route: QueryRoute | MutationRoute) =>
|
|
21
|
-
(args: RouteArgs): RouteRequest => {
|
|
22
|
-
return {
|
|
23
|
-
route,
|
|
24
|
-
pathPattern,
|
|
25
|
-
method,
|
|
26
|
-
args,
|
|
27
|
-
$result: undefined!,
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
return Object.assign(
|
|
32
|
-
{ path, pathPattern, routes },
|
|
33
|
-
mapEntries(
|
|
34
|
-
routes as Record<string, QueryRoute | MutationRoute>,
|
|
35
|
-
(method, route) => [method, createFetch(method, route)]
|
|
36
|
-
)
|
|
37
|
-
) as unknown as {
|
|
38
|
-
path: P
|
|
39
|
-
pathPattern: RoutePattern
|
|
40
|
-
routes: T
|
|
41
|
-
} & {
|
|
42
|
-
[K in keyof T]: RouteFunction<Extract<T[K], QueryRoute | MutationRoute>>
|
|
43
|
-
}
|
|
44
|
-
}
|
package/src/server/router.ts
DELETED
|
@@ -1,248 +0,0 @@
|
|
|
1
|
-
import type { AdapterRequestContext } from '@hattip/core'
|
|
2
|
-
import { RoutePattern, type Params } from '@remix-run/route-pattern'
|
|
3
|
-
import {
|
|
4
|
-
chain,
|
|
5
|
-
MiddlewareChain,
|
|
6
|
-
type MiddlewareContext,
|
|
7
|
-
} from 'alien-middleware'
|
|
8
|
-
import { mapValues } from '../common'
|
|
9
|
-
import * as z from 'zod/mini'
|
|
10
|
-
import type {
|
|
11
|
-
InferRouteResponse,
|
|
12
|
-
MutationRoute,
|
|
13
|
-
Promisable,
|
|
14
|
-
QueryRoute,
|
|
15
|
-
Routes,
|
|
16
|
-
} from '../types'
|
|
17
|
-
|
|
18
|
-
export { chain }
|
|
19
|
-
|
|
20
|
-
type EmptyMiddlewareChain<TPlatform = unknown> = MiddlewareChain<{
|
|
21
|
-
initial: {
|
|
22
|
-
env: {}
|
|
23
|
-
properties: {}
|
|
24
|
-
}
|
|
25
|
-
current: {
|
|
26
|
-
env: {}
|
|
27
|
-
properties: {}
|
|
28
|
-
}
|
|
29
|
-
platform: TPlatform
|
|
30
|
-
}>
|
|
31
|
-
|
|
32
|
-
export type RouterConfig<
|
|
33
|
-
TRoutes extends Record<string, { path: string; routes: Routes }> = any,
|
|
34
|
-
TMiddleware extends MiddlewareChain = any,
|
|
35
|
-
> = {
|
|
36
|
-
routes: TRoutes
|
|
37
|
-
middlewares?: TMiddleware
|
|
38
|
-
debug?: boolean
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export function createRouter<
|
|
42
|
-
TRoutes extends Record<string, { path: string; routes: Routes }>,
|
|
43
|
-
TMiddleware extends MiddlewareChain = EmptyMiddlewareChain,
|
|
44
|
-
>(config: { routes: TRoutes; middlewares?: TMiddleware; debug?: boolean }) {
|
|
45
|
-
const keys = Object.keys(config.routes)
|
|
46
|
-
const middlewares = config.middlewares ?? (chain() as TMiddleware)
|
|
47
|
-
const patterns = mapValues(
|
|
48
|
-
config.routes,
|
|
49
|
-
({ path }) => new RoutePattern(path)
|
|
50
|
-
)
|
|
51
|
-
|
|
52
|
-
type RequestContext = MiddlewareContext<TMiddleware>
|
|
53
|
-
|
|
54
|
-
type RequestHandler<TArgs extends object, TResult> = (
|
|
55
|
-
context: RequestContext & TArgs
|
|
56
|
-
) => Promisable<TResult | Response>
|
|
57
|
-
|
|
58
|
-
type InferRequestHandler<T, P extends string> = T extends QueryRoute
|
|
59
|
-
? RequestHandler<
|
|
60
|
-
{
|
|
61
|
-
query: z.infer<T['query']>
|
|
62
|
-
params: Params<P>
|
|
63
|
-
headers: z.infer<T['headers']>
|
|
64
|
-
},
|
|
65
|
-
InferRouteResponse<T>
|
|
66
|
-
>
|
|
67
|
-
: T extends MutationRoute
|
|
68
|
-
? RequestHandler<
|
|
69
|
-
{
|
|
70
|
-
body: z.infer<T['body']>
|
|
71
|
-
params: Params<P>
|
|
72
|
-
headers: z.infer<T['headers']>
|
|
73
|
-
},
|
|
74
|
-
InferRouteResponse<T>
|
|
75
|
-
>
|
|
76
|
-
: never
|
|
77
|
-
|
|
78
|
-
type RequestHandlers = {
|
|
79
|
-
[K in keyof TRoutes]: {
|
|
80
|
-
[M in keyof TRoutes[K]['routes']]: InferRequestHandler<
|
|
81
|
-
TRoutes[K]['routes'][M],
|
|
82
|
-
TRoutes[K]['path']
|
|
83
|
-
>
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
type TPlatform =
|
|
88
|
-
TMiddleware extends MiddlewareChain<infer T> ? T['platform'] : never
|
|
89
|
-
|
|
90
|
-
return (handlers: RequestHandlers) =>
|
|
91
|
-
middlewares.use(async function (
|
|
92
|
-
context: AdapterRequestContext<TPlatform> & {
|
|
93
|
-
url?: URL
|
|
94
|
-
params?: {}
|
|
95
|
-
}
|
|
96
|
-
) {
|
|
97
|
-
const request = context.request as Request
|
|
98
|
-
const method = request.method.toUpperCase() as keyof Routes
|
|
99
|
-
const url: URL = (context.url ??= new URL(request.url))
|
|
100
|
-
|
|
101
|
-
for (let i = 0; i < keys.length; i++) {
|
|
102
|
-
const pattern = patterns[keys[i]]
|
|
103
|
-
|
|
104
|
-
const match = pattern.match(url)
|
|
105
|
-
if (!match) {
|
|
106
|
-
continue
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const route = config.routes[keys[i]].routes[method]
|
|
110
|
-
if (!route) {
|
|
111
|
-
continue
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
if (route.headers) {
|
|
115
|
-
const error = parseHeaders(
|
|
116
|
-
context,
|
|
117
|
-
enableStringParsing(route.headers)
|
|
118
|
-
)
|
|
119
|
-
if (error) {
|
|
120
|
-
return httpClientError(error, 'Invalid request headers', config)
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
if (route.query) {
|
|
125
|
-
const error = parseQueryString(
|
|
126
|
-
context,
|
|
127
|
-
enableStringParsing(route.query)
|
|
128
|
-
)
|
|
129
|
-
if (error) {
|
|
130
|
-
return httpClientError(error, 'Invalid query string', config)
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
if (route.body) {
|
|
135
|
-
const error = await parseRequestBody(context, route.body)
|
|
136
|
-
if (error) {
|
|
137
|
-
return httpClientError(error, 'Invalid request body', config)
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const handler = handlers[keys[i]][method]
|
|
142
|
-
if (!handler) {
|
|
143
|
-
continue
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
context.params = match.params
|
|
147
|
-
|
|
148
|
-
const result = await handler(context as any)
|
|
149
|
-
if (result instanceof Response) {
|
|
150
|
-
return result
|
|
151
|
-
}
|
|
152
|
-
return Response.json(result)
|
|
153
|
-
}
|
|
154
|
-
})
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
function httpClientError(
|
|
158
|
-
error: any,
|
|
159
|
-
message: string,
|
|
160
|
-
config: { debug?: boolean }
|
|
161
|
-
) {
|
|
162
|
-
return Response.json(
|
|
163
|
-
{
|
|
164
|
-
...error,
|
|
165
|
-
message: config.debug ? `${message}: ${error.message}` : message,
|
|
166
|
-
},
|
|
167
|
-
{ status: 400 }
|
|
168
|
-
)
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function parseHeaders(
|
|
172
|
-
context: AdapterRequestContext & { headers?: {} },
|
|
173
|
-
schema: z.ZodMiniType<any, any>
|
|
174
|
-
) {
|
|
175
|
-
const headers = Object.fromEntries(context.request.headers as any)
|
|
176
|
-
const result = schema.safeParse(headers)
|
|
177
|
-
if (!result.success) {
|
|
178
|
-
return result.error
|
|
179
|
-
}
|
|
180
|
-
context.headers = result.data
|
|
181
|
-
return null
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function parseQueryString(
|
|
185
|
-
context: AdapterRequestContext & { url?: URL; query?: {} },
|
|
186
|
-
schema: z.ZodMiniType<any, any>
|
|
187
|
-
) {
|
|
188
|
-
const result = schema.safeParse(
|
|
189
|
-
Object.fromEntries(context.url!.searchParams as any)
|
|
190
|
-
)
|
|
191
|
-
if (!result.success) {
|
|
192
|
-
return result.error
|
|
193
|
-
}
|
|
194
|
-
context.query = result.data
|
|
195
|
-
return null
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
async function parseRequestBody(
|
|
199
|
-
context: AdapterRequestContext & { body?: {} },
|
|
200
|
-
schema: z.ZodMiniType<any, any>
|
|
201
|
-
) {
|
|
202
|
-
const result = await context.request.json().then(
|
|
203
|
-
body => schema.safeParse(body),
|
|
204
|
-
error => ({ success: false, error }) as const
|
|
205
|
-
)
|
|
206
|
-
if (!result.success) {
|
|
207
|
-
return result.error
|
|
208
|
-
}
|
|
209
|
-
context.body = result.data
|
|
210
|
-
return null
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
const seen = new WeakMap<z.ZodMiniType<any, any>, z.ZodMiniType<any, any>>()
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* Traverse object and array schemas, finding schemas that expect a number or
|
|
217
|
-
* boolean, and replace those schemas with a new schema that parses the input
|
|
218
|
-
* value as a number or boolean.
|
|
219
|
-
*/
|
|
220
|
-
function enableStringParsing(schema: z.ZodMiniType<any, any>): typeof schema {
|
|
221
|
-
if (schema.type === 'number') {
|
|
222
|
-
return z.pipe(z.transform(Number), schema)
|
|
223
|
-
}
|
|
224
|
-
if (schema.type === 'boolean') {
|
|
225
|
-
return z.pipe(z.transform(toBooleanStrict), schema)
|
|
226
|
-
}
|
|
227
|
-
if (schema.type === 'object') {
|
|
228
|
-
const cached = seen.get(schema)
|
|
229
|
-
if (cached) {
|
|
230
|
-
return cached
|
|
231
|
-
}
|
|
232
|
-
const modified = z.object(
|
|
233
|
-
mapValues((schema as z.ZodMiniObject<any>).def.shape, enableStringParsing)
|
|
234
|
-
)
|
|
235
|
-
seen.set(schema, modified)
|
|
236
|
-
return modified
|
|
237
|
-
}
|
|
238
|
-
if (schema.type === 'array') {
|
|
239
|
-
return z.array(
|
|
240
|
-
enableStringParsing((schema as z.ZodMiniArray<any>).def.element)
|
|
241
|
-
)
|
|
242
|
-
}
|
|
243
|
-
return schema
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
function toBooleanStrict(value: string) {
|
|
247
|
-
return value === 'true' || (value === 'false' ? false : value)
|
|
248
|
-
}
|
package/src/types.ts
DELETED
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import { Params, RoutePattern } from '@remix-run/route-pattern'
|
|
2
|
-
import * as z from 'zod/mini'
|
|
3
|
-
|
|
4
|
-
export type Promisable<T> = T | Promise<T>
|
|
5
|
-
|
|
6
|
-
export type Unchecked<T> = { __unchecked__: T }
|
|
7
|
-
|
|
8
|
-
export type QueryRoute = {
|
|
9
|
-
path?: z.ZodMiniObject<any>
|
|
10
|
-
query?: z.ZodMiniObject<any>
|
|
11
|
-
body?: never
|
|
12
|
-
headers?: z.ZodMiniObject<any>
|
|
13
|
-
response: Unchecked<any>
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export type MutationRoute = {
|
|
17
|
-
path?: z.ZodMiniObject<any>
|
|
18
|
-
query?: never
|
|
19
|
-
body: z.ZodMiniType<any, any>
|
|
20
|
-
headers?: z.ZodMiniObject<any>
|
|
21
|
-
response?: Unchecked<any>
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export type Routes = {
|
|
25
|
-
GET?: QueryRoute
|
|
26
|
-
POST?: MutationRoute
|
|
27
|
-
PUT?: MutationRoute
|
|
28
|
-
PATCH?: MutationRoute
|
|
29
|
-
DELETE?: MutationRoute
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
declare class Any {
|
|
33
|
-
private isAny: true
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
type PathArgs<T> = T extends { path: infer TPath extends string }
|
|
37
|
-
? Params<TPath> extends infer TParams
|
|
38
|
-
? {} extends TParams
|
|
39
|
-
? { path?: TParams }
|
|
40
|
-
: { path: TParams }
|
|
41
|
-
: unknown
|
|
42
|
-
: unknown
|
|
43
|
-
|
|
44
|
-
type QueryArgs<T> = T extends QueryRoute & { query: infer TQuery }
|
|
45
|
-
? {} extends z.infer<TQuery>
|
|
46
|
-
? { query?: z.infer<TQuery> }
|
|
47
|
-
: { query: z.infer<TQuery> }
|
|
48
|
-
: unknown
|
|
49
|
-
|
|
50
|
-
type MutationArgs<T> = T extends MutationRoute & { body: infer TBody }
|
|
51
|
-
? {} extends z.infer<TBody>
|
|
52
|
-
? { body?: z.infer<TBody> }
|
|
53
|
-
: { body: z.infer<TBody> }
|
|
54
|
-
: unknown
|
|
55
|
-
|
|
56
|
-
export type RouteArgs<T extends QueryRoute | MutationRoute = any> = ([
|
|
57
|
-
T,
|
|
58
|
-
] extends [Any]
|
|
59
|
-
? { query?: any; body?: any; path?: any }
|
|
60
|
-
: QueryArgs<T> & MutationArgs<T> & PathArgs<T>) &
|
|
61
|
-
Omit<RequestInit, 'method' | 'body' | 'headers'> & {
|
|
62
|
-
headers?: Record<string, string | undefined>
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export type RouteRequest<TResult = any> = {
|
|
66
|
-
route: QueryRoute | MutationRoute
|
|
67
|
-
pathPattern: RoutePattern
|
|
68
|
-
method: string
|
|
69
|
-
args: RouteArgs
|
|
70
|
-
$result: TResult
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export type RouteResponse<TResult = any> = Response & {
|
|
74
|
-
json(): Promise<TResult>
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export type InferRouteResponse<T extends QueryRoute | MutationRoute> =
|
|
78
|
-
T extends {
|
|
79
|
-
response: Unchecked<infer TResponse>
|
|
80
|
-
}
|
|
81
|
-
? TResponse
|
|
82
|
-
: void
|
|
83
|
-
|
|
84
|
-
export type RouteFunction<T extends QueryRoute | MutationRoute> = {
|
|
85
|
-
(args: RouteArgs<T>): RouteRequest<InferRouteResponse<T>>
|
|
86
|
-
|
|
87
|
-
$args: RouteArgs<T>
|
|
88
|
-
$response: InferRouteResponse<T>
|
|
89
|
-
}
|