rouzer 4.0.0 → 5.1.0
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/README.md +8 -6
- package/dist/client/index.d.ts +12 -5
- package/dist/client/index.js +58 -39
- package/dist/http.d.ts +4 -1
- package/dist/http.js +7 -0
- package/dist/response-map.js +1 -1
- package/dist/response.d.ts +3 -2
- package/dist/server/router.d.ts +1 -1
- package/dist/server/router.js +2 -1
- package/dist/types/args.d.ts +29 -37
- package/dist/types/handler.d.ts +6 -7
- package/dist/types/schema.d.ts +6 -2
- package/dist/types/server.d.ts +2 -2
- package/docs/context.md +47 -23
- package/examples/basic-usage.ts +2 -9
- package/examples/error-responses.ts +4 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -97,14 +97,16 @@ const client = createClient({
|
|
|
97
97
|
})
|
|
98
98
|
|
|
99
99
|
const { message } = await client.hello({
|
|
100
|
-
|
|
101
|
-
|
|
100
|
+
name: 'world',
|
|
101
|
+
excited: true,
|
|
102
102
|
})
|
|
103
103
|
```
|
|
104
104
|
|
|
105
105
|
`handler` can be mounted with any Hattip adapter. Generated client action calls
|
|
106
|
-
validate route arguments before `fetch`; server handlers validate matched
|
|
107
|
-
query, headers, and JSON bodies before your handler runs.
|
|
106
|
+
validate flat route arguments before `fetch`; server handlers validate matched
|
|
107
|
+
path, query, headers, and JSON bodies before your handler runs. Per-request
|
|
108
|
+
headers, abort signals, and other `RequestInit` options are passed as a second
|
|
109
|
+
client action argument.
|
|
108
110
|
|
|
109
111
|
### Typed status responses
|
|
110
112
|
|
|
@@ -143,7 +145,7 @@ const client = createClient({
|
|
|
143
145
|
routes,
|
|
144
146
|
})
|
|
145
147
|
|
|
146
|
-
const [error, user, status] = await client.getUser({
|
|
148
|
+
const [error, user, status] = await client.getUser({ id: '42' })
|
|
147
149
|
```
|
|
148
150
|
|
|
149
151
|
Success entries resolve as `[null, value, status]`; declared error entries
|
|
@@ -185,7 +187,7 @@ for await (const event of await client.events()) {
|
|
|
185
187
|
|
|
186
188
|
## Documentation
|
|
187
189
|
|
|
188
|
-
- [Concepts, API selection, and
|
|
190
|
+
- [Concepts, API selection, v5 client input notes, and migration notes](docs/context.md)
|
|
189
191
|
- [Runnable shared-route example](examples/basic-usage.ts)
|
|
190
192
|
- [Runnable typed error response example](examples/error-responses.ts)
|
|
191
193
|
- [Runnable NDJSON response-stream example](examples/ndjson-stream.ts)
|
package/dist/client/index.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Promisable } from '../common.js';
|
|
2
|
-
import type
|
|
2
|
+
import { type HttpAction, type HttpResource, type HttpRouteTree } from '../http.js';
|
|
3
3
|
import { type ClientResponsePlugin } from '../response.js';
|
|
4
|
-
import type {
|
|
4
|
+
import type { RouteFetchOptions, RouteInput, RouteOptions } from '../types/args.js';
|
|
5
|
+
import type { RawBodySchema } from '../types/schema.js';
|
|
5
6
|
import type { InferRouteResponse } from '../types/response.js';
|
|
6
7
|
import type { RouteSchema } from '../types/schema.js';
|
|
7
8
|
/** Client type inferred from an HTTP route tree passed to `createClient`. */
|
|
@@ -33,7 +34,7 @@ export declare function createClient<TRoutes extends HttpRouteTree = Record<stri
|
|
|
33
34
|
* @example
|
|
34
35
|
* ```ts
|
|
35
36
|
* const client = createClient({ baseURL: 'https://example.com/api/', routes })
|
|
36
|
-
* await client.users.list({
|
|
37
|
+
* await client.users.list({ page: 1 })
|
|
37
38
|
* ```
|
|
38
39
|
*/
|
|
39
40
|
routes: TRoutes;
|
|
@@ -72,7 +73,7 @@ export declare function createClient<TRoutes extends HttpRouteTree = Record<stri
|
|
|
72
73
|
* @example
|
|
73
74
|
* ```ts
|
|
74
75
|
* const client = createClient({ baseURL: 'https://example.com/api/', routes })
|
|
75
|
-
* await client.users.list({
|
|
76
|
+
* await client.users.list({ page: 1 })
|
|
76
77
|
* ```
|
|
77
78
|
*/
|
|
78
79
|
routes: TRoutes;
|
|
@@ -106,7 +107,13 @@ export type ClientTree<T extends HttpRouteTree, TPrefix extends string = ''> = {
|
|
|
106
107
|
* plugin's client result type. Actions without a response marker return the raw
|
|
107
108
|
* `Response`.
|
|
108
109
|
*/
|
|
109
|
-
export type RouteFunction<T extends RouteSchema, P extends string> =
|
|
110
|
+
export type RouteFunction<T extends RouteSchema, P extends string> = T extends {
|
|
111
|
+
body: RawBodySchema;
|
|
112
|
+
} ? RouteInput<T, P> extends infer TInput ? {} extends TInput ? (body: BodyInit | null, options?: RouteFetchOptions<T>) => Promise<T extends {
|
|
113
|
+
response: any;
|
|
114
|
+
} ? InferRouteResponse<T> : Response> : (input: TInput, options: RouteOptions<T>) => Promise<T extends {
|
|
115
|
+
response: any;
|
|
116
|
+
} ? InferRouteResponse<T> : Response> : never : (...p: RouteInput<T, P> extends infer TInput ? {} extends TInput ? [input?: TInput, options?: RouteOptions<T>] : [input: TInput, options?: RouteOptions<T>] : never) => Promise<T extends {
|
|
110
117
|
response: any;
|
|
111
118
|
} ? InferRouteResponse<T> : Response>;
|
|
112
119
|
export {};
|
package/dist/client/index.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { RoutePattern } from '@remix-run/route-pattern';
|
|
2
2
|
import { createHref } from '@remix-run/route-pattern/href';
|
|
3
3
|
import { shake } from '../common.js';
|
|
4
|
-
import {
|
|
4
|
+
import { isRawBodySchema, } from '../http.js';
|
|
5
5
|
import { getResponseMapPluginIds, isErrorMarker, isResponseMap, } from '../response-map.js';
|
|
6
|
+
import { createResponsePluginMap, getResponsePluginMarkerId, } from '../response.js';
|
|
6
7
|
/**
|
|
7
8
|
* Create a typed fetch client for an HTTP route tree.
|
|
8
9
|
*
|
|
@@ -15,13 +16,12 @@ export function createClient(config) {
|
|
|
15
16
|
const fetch = config.fetch ?? globalThis.fetch;
|
|
16
17
|
const responsePlugins = createResponsePluginMap(config.plugins, 'client response');
|
|
17
18
|
validateClientResponsePlugins(config.routes, responsePlugins);
|
|
18
|
-
async function
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
19
|
+
async function plainRequest({ path: pathPattern, method, input = {}, options: { body: rawBody, headers, ...init } = {}, schema, }) {
|
|
20
|
+
const path = schema.path
|
|
21
|
+
? schema.path.parse(pickObjectSchemaFields(schema.path, input))
|
|
22
|
+
: input;
|
|
23
23
|
let url;
|
|
24
|
-
const href = createHref(
|
|
24
|
+
const href = createHref(pathPattern, path);
|
|
25
25
|
if (href[0] === '/') {
|
|
26
26
|
url = new URL(baseURL);
|
|
27
27
|
url.pathname += href.slice(1);
|
|
@@ -30,17 +30,14 @@ export function createClient(config) {
|
|
|
30
30
|
url = new URL(href, baseURL);
|
|
31
31
|
}
|
|
32
32
|
if (schema.query) {
|
|
33
|
-
query = schema.query.parse(query
|
|
33
|
+
const query = schema.query.parse(pickObjectSchemaFields(schema.query, input));
|
|
34
34
|
url.search = new URLSearchParams(shake(query)).toString();
|
|
35
35
|
}
|
|
36
|
-
|
|
37
|
-
throw new Error('Unexpected query parameters');
|
|
38
|
-
}
|
|
36
|
+
let body;
|
|
39
37
|
if (schema.body) {
|
|
40
|
-
body = schema.body
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
throw new Error('Unexpected body');
|
|
38
|
+
body = isRawBodySchema(schema.body)
|
|
39
|
+
? rawBody
|
|
40
|
+
: schema.body.parse(pickObjectSchemaFields(schema.body, input));
|
|
44
41
|
}
|
|
45
42
|
if (headers) {
|
|
46
43
|
headers = shake(headers);
|
|
@@ -54,20 +51,24 @@ export function createClient(config) {
|
|
|
54
51
|
return fetch(url, {
|
|
55
52
|
...init,
|
|
56
53
|
method,
|
|
57
|
-
body:
|
|
54
|
+
body: isRawBodySchema(schema.body)
|
|
55
|
+
? body
|
|
56
|
+
: body !== undefined
|
|
57
|
+
? JSON.stringify(body)
|
|
58
|
+
: undefined,
|
|
58
59
|
headers: (headers ?? defaultHeaders),
|
|
59
60
|
});
|
|
60
61
|
}
|
|
61
|
-
async function
|
|
62
|
-
const
|
|
62
|
+
async function parsedRequest(props) {
|
|
63
|
+
const response = await plainRequest(props);
|
|
63
64
|
const responseSchema = props.schema.response;
|
|
64
65
|
// Handle status-keyed response maps
|
|
65
66
|
if (isResponseMap(responseSchema)) {
|
|
66
|
-
const status =
|
|
67
|
+
const status = response.status;
|
|
67
68
|
if (status in responseSchema) {
|
|
68
69
|
const marker = responseSchema[status];
|
|
69
70
|
if (isErrorMarker(marker)) {
|
|
70
|
-
return [await
|
|
71
|
+
return [await response.json(), null, status];
|
|
71
72
|
}
|
|
72
73
|
const pluginId = getResponsePluginMarkerId(marker);
|
|
73
74
|
if (pluginId) {
|
|
@@ -77,20 +78,20 @@ export function createClient(config) {
|
|
|
77
78
|
}
|
|
78
79
|
return [
|
|
79
80
|
null,
|
|
80
|
-
await plugin.decode(
|
|
81
|
+
await plugin.decode(response, {
|
|
81
82
|
marker: marker,
|
|
82
83
|
request: props,
|
|
83
84
|
}),
|
|
84
85
|
status,
|
|
85
86
|
];
|
|
86
87
|
}
|
|
87
|
-
return [null, await
|
|
88
|
+
return [null, await response.json(), status];
|
|
88
89
|
}
|
|
89
90
|
// Undeclared status — reject
|
|
90
|
-
return handleResponseError(
|
|
91
|
+
return handleResponseError(response, props);
|
|
91
92
|
}
|
|
92
|
-
if (!
|
|
93
|
-
return handleResponseError(
|
|
93
|
+
if (!response.ok) {
|
|
94
|
+
return handleResponseError(response, props);
|
|
94
95
|
}
|
|
95
96
|
const pluginId = getResponsePluginMarkerId(responseSchema);
|
|
96
97
|
if (pluginId) {
|
|
@@ -98,18 +99,18 @@ export function createClient(config) {
|
|
|
98
99
|
if (!plugin) {
|
|
99
100
|
throw missingClientResponsePlugin(pluginId);
|
|
100
101
|
}
|
|
101
|
-
return plugin.decode(
|
|
102
|
+
return plugin.decode(response, {
|
|
102
103
|
marker: responseSchema,
|
|
103
104
|
request: props,
|
|
104
105
|
});
|
|
105
106
|
}
|
|
106
|
-
return
|
|
107
|
+
return response.json();
|
|
107
108
|
}
|
|
108
109
|
async function handleResponseError(response, props) {
|
|
109
110
|
if (config.onJsonError) {
|
|
110
111
|
return config.onJsonError(response);
|
|
111
112
|
}
|
|
112
|
-
const error = new Error(`Request to ${props.method} ${createHref(props.path, props.
|
|
113
|
+
const error = new Error(`Request to ${props.method} ${createHref(props.path, props.input)} failed with status ${response.status}`);
|
|
113
114
|
const contentType = response.headers.get('content-type');
|
|
114
115
|
if (contentType?.includes('application/json')) {
|
|
115
116
|
Object.assign(error, await response.json());
|
|
@@ -117,29 +118,36 @@ export function createClient(config) {
|
|
|
117
118
|
throw error;
|
|
118
119
|
}
|
|
119
120
|
return {
|
|
120
|
-
...connectTree(config.routes, '',
|
|
121
|
+
...connectTree(config.routes, '', plainRequest, parsedRequest),
|
|
121
122
|
clientConfig: config,
|
|
122
123
|
};
|
|
123
124
|
}
|
|
124
|
-
function connectTree(tree, prefix,
|
|
125
|
+
function connectTree(tree, prefix, plainRequest, parsedRequest) {
|
|
125
126
|
return Object.fromEntries(Object.entries(tree).map(([key, node]) => {
|
|
126
127
|
if (node.kind === 'resource') {
|
|
127
128
|
return [
|
|
128
129
|
key,
|
|
129
|
-
connectTree(node.children, joinPaths(prefix, node.path.source),
|
|
130
|
+
connectTree(node.children, joinPaths(prefix, node.path.source), plainRequest, parsedRequest),
|
|
130
131
|
];
|
|
131
132
|
}
|
|
132
133
|
const path = RoutePattern.parse(joinPaths(prefix, node.path?.source ?? ''));
|
|
133
|
-
const fetch = node.schema.response ?
|
|
134
|
+
const fetch = node.schema.response ? parsedRequest : plainRequest;
|
|
134
135
|
return [
|
|
135
136
|
key,
|
|
136
|
-
(
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
137
|
+
(input, options) => {
|
|
138
|
+
if (isRawBodySchema(node.schema.body) && !hasRouteInput(node, path)) {
|
|
139
|
+
options = { ...options, body: input };
|
|
140
|
+
input = undefined;
|
|
141
|
+
}
|
|
142
|
+
return fetch({
|
|
143
|
+
schema: node.schema,
|
|
144
|
+
path,
|
|
145
|
+
method: node.method,
|
|
146
|
+
input,
|
|
147
|
+
options,
|
|
148
|
+
$result: undefined,
|
|
149
|
+
});
|
|
150
|
+
},
|
|
143
151
|
];
|
|
144
152
|
}));
|
|
145
153
|
}
|
|
@@ -163,6 +171,17 @@ function validateClientResponsePlugins(tree, plugins) {
|
|
|
163
171
|
function missingClientResponsePlugin(pluginId) {
|
|
164
172
|
return new Error(`Missing client response plugin for ${pluginId}`);
|
|
165
173
|
}
|
|
174
|
+
function hasRouteInput(node, path) {
|
|
175
|
+
return Boolean(node.schema.path || node.schema.query || /(^|\/)[:*]/.test(path.source));
|
|
176
|
+
}
|
|
177
|
+
function pickObjectSchemaFields(schema, input) {
|
|
178
|
+
if (typeof input !== 'object' || input === null) {
|
|
179
|
+
return input;
|
|
180
|
+
}
|
|
181
|
+
return Object.fromEntries(Object.keys(schema.shape)
|
|
182
|
+
.filter(key => key in input)
|
|
183
|
+
.map(key => [key, input[key]]));
|
|
184
|
+
}
|
|
166
185
|
function joinPaths(left, right) {
|
|
167
186
|
return [left, right].filter(Boolean).join('/').replace(/\/+/g, '/');
|
|
168
187
|
}
|
package/dist/http.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { RoutePattern } from '@remix-run/route-pattern';
|
|
2
|
-
import type { RouteSchema } from './types/schema.js';
|
|
2
|
+
import type { RawBodySchema, RouteSchema } from './types/schema.js';
|
|
3
3
|
/** HTTP methods supported by Rouzer action declarations. */
|
|
4
4
|
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
5
5
|
/**
|
|
@@ -62,3 +62,6 @@ export declare function patch<const T extends RouteSchema>(schema: T): HttpActio
|
|
|
62
62
|
declare function deleteAction<const P extends string, const T extends RouteSchema>(path: P, schema: T): HttpAction<P, T, 'DELETE'>;
|
|
63
63
|
declare function deleteAction<const T extends RouteSchema>(schema: T): HttpAction<'', T, 'DELETE'>;
|
|
64
64
|
export { deleteAction as delete };
|
|
65
|
+
/** Declare a request body that is passed through to `fetch` without JSON encoding. */
|
|
66
|
+
export declare function rawBody(): RawBodySchema;
|
|
67
|
+
export declare function isRawBodySchema(schema: unknown): schema is RawBodySchema;
|
package/dist/http.js
CHANGED
|
@@ -29,6 +29,13 @@ function deleteAction(pathOrSchema, schema) {
|
|
|
29
29
|
return action('DELETE', pathOrSchema, schema);
|
|
30
30
|
}
|
|
31
31
|
export { deleteAction as delete };
|
|
32
|
+
/** Declare a request body that is passed through to `fetch` without JSON encoding. */
|
|
33
|
+
export function rawBody() {
|
|
34
|
+
return { __rawBody__: Symbol('rouzer.rawBody') };
|
|
35
|
+
}
|
|
36
|
+
export function isRawBodySchema(schema) {
|
|
37
|
+
return Boolean(schema && typeof schema === 'object' && '__rawBody__' in schema);
|
|
38
|
+
}
|
|
32
39
|
function action(method, pathOrSchema, schema) {
|
|
33
40
|
const path = typeof pathOrSchema === 'string'
|
|
34
41
|
? RoutePattern.parse(pathOrSchema)
|
package/dist/response-map.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getResponsePluginMarkerId, responsePluginMarker
|
|
1
|
+
import { getResponsePluginMarkerId, responsePluginMarker } from './response.js';
|
|
2
2
|
import { $error } from './type.js';
|
|
3
3
|
/** Return true when the response schema is a status-keyed response map. */
|
|
4
4
|
export function isResponseMap(response) {
|
package/dist/response.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Promisable } from './common.js';
|
|
2
2
|
import type { RoutePattern } from '@remix-run/route-pattern';
|
|
3
|
-
import type {
|
|
3
|
+
import type { RouteOptions } from './types/args.js';
|
|
4
4
|
import type { RouteSchema } from './types/schema.js';
|
|
5
5
|
/** Runtime key carried by response plugin markers. */
|
|
6
6
|
export declare const responsePluginMarker: unique symbol;
|
|
@@ -34,7 +34,8 @@ export type ClientResponsePluginRequest = {
|
|
|
34
34
|
schema: RouteSchema;
|
|
35
35
|
path: RoutePattern;
|
|
36
36
|
method: string;
|
|
37
|
-
|
|
37
|
+
input?: unknown;
|
|
38
|
+
options?: RouteOptions;
|
|
38
39
|
};
|
|
39
40
|
/** Router-side response plugin used by `createRouter({ plugins })`. */
|
|
40
41
|
export type RouterResponsePlugin = {
|
package/dist/server/router.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { HattipHandler } from '@hattip/core';
|
|
2
2
|
import { ApplyMiddleware, chain, ExtractMiddleware, MiddlewareChain, MiddlewareTypes } from 'alien-middleware';
|
|
3
|
-
import type
|
|
3
|
+
import { type HttpRouteTree } from '../http.js';
|
|
4
4
|
import { type RouterResponsePlugin } from '../response.js';
|
|
5
5
|
import type { RouteRequestHandlerMap } from '../types/server.js';
|
|
6
6
|
export { chain };
|
package/dist/server/router.js
CHANGED
|
@@ -3,6 +3,7 @@ import { createMatcher } from '@remix-run/route-pattern/match';
|
|
|
3
3
|
import { chain, MiddlewareChain, } from 'alien-middleware';
|
|
4
4
|
import * as z from 'zod';
|
|
5
5
|
import { mapValues } from '../common.js';
|
|
6
|
+
import { isRawBodySchema } from '../http.js';
|
|
6
7
|
import { createResponsePluginMap, getResponsePluginMarkerId, } from '../response.js';
|
|
7
8
|
import { getDefaultSuccessStatus, getResponseMapPluginIds, isErrorMarker, isResponseMap, } from '../response-map.js';
|
|
8
9
|
export { chain };
|
|
@@ -103,7 +104,7 @@ class RouterObject extends MiddlewareChain {
|
|
|
103
104
|
return httpClientError(error, 'Invalid query string', config);
|
|
104
105
|
}
|
|
105
106
|
}
|
|
106
|
-
if (schema.body) {
|
|
107
|
+
if (schema.body && !isRawBodySchema(schema.body)) {
|
|
107
108
|
const error = await parseRequestBody(context, schema.body);
|
|
108
109
|
if (error) {
|
|
109
110
|
addDebugHeaders?.(context, route);
|
package/dist/types/args.d.ts
CHANGED
|
@@ -1,51 +1,43 @@
|
|
|
1
1
|
import type { MatchParams } from '@remix-run/route-pattern/match';
|
|
2
2
|
import type * as z from 'zod';
|
|
3
|
-
import type { MutationRouteSchema, QueryRouteSchema, RouteSchema } from './schema.js';
|
|
3
|
+
import type { MutationRouteSchema, QueryRouteSchema, RawBodySchema, RouteSchema } from './schema.js';
|
|
4
4
|
declare class Any {
|
|
5
5
|
private isAny;
|
|
6
6
|
}
|
|
7
|
-
type
|
|
7
|
+
type PathInput<T, P extends string> = T extends {
|
|
8
8
|
path: infer TPath;
|
|
9
|
-
} ?
|
|
10
|
-
|
|
11
|
-
} : {
|
|
12
|
-
[K in keyof T as 'path']: z.infer<TPath>;
|
|
13
|
-
} : MatchParams<P> extends infer TParams ? {} extends TParams ? {
|
|
14
|
-
[K in keyof T as 'path']?: TParams;
|
|
15
|
-
} : {
|
|
16
|
-
[K in keyof T as 'path']: TParams;
|
|
17
|
-
} : unknown;
|
|
18
|
-
type QueryArgs<T> = T extends QueryRouteSchema & {
|
|
9
|
+
} ? z.infer<TPath> : MatchParams<P>;
|
|
10
|
+
type QueryInput<T> = T extends QueryRouteSchema & {
|
|
19
11
|
query: infer TQuery;
|
|
20
|
-
} ?
|
|
21
|
-
|
|
22
|
-
} : {
|
|
23
|
-
[K in keyof T as 'query']: z.infer<TQuery>;
|
|
24
|
-
} : unknown;
|
|
25
|
-
type MutationArgs<T> = T extends MutationRouteSchema ? T extends {
|
|
12
|
+
} ? z.infer<TQuery> : unknown;
|
|
13
|
+
type BodyInput<T> = T extends MutationRouteSchema ? T extends {
|
|
26
14
|
body: infer TBody;
|
|
27
|
-
} ?
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
} : {
|
|
32
|
-
body?: unknown;
|
|
33
|
-
} : unknown;
|
|
15
|
+
} ? TBody extends RawBodySchema ? unknown : z.infer<TBody> : unknown : unknown;
|
|
16
|
+
type HeaderInput<T> = T extends {
|
|
17
|
+
headers: infer THeaders;
|
|
18
|
+
} ? Partial<z.infer<THeaders>> : Record<string, string | undefined>;
|
|
34
19
|
/**
|
|
35
|
-
*
|
|
20
|
+
* Semantic input accepted by a generated client action function.
|
|
36
21
|
*
|
|
37
|
-
* @remarks
|
|
38
|
-
*
|
|
39
|
-
* a
|
|
40
|
-
* except `method`, `body`, and `headers`, which Rouzer derives from the action
|
|
41
|
-
* schema and call arguments.
|
|
22
|
+
* @remarks Path params, query params, and JSON body fields are flattened into a
|
|
23
|
+
* single object. Avoid declaring duplicate keys across path/query/body schemas,
|
|
24
|
+
* since a flat input cannot distinguish their source.
|
|
42
25
|
*/
|
|
43
|
-
export type
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
26
|
+
export type RouteInput<T extends RouteSchema = any, P extends string = string> = [T] extends [Any] ? any : PathInput<T, P> & QueryInput<T> & BodyInput<T>;
|
|
27
|
+
/**
|
|
28
|
+
* Fetch options accepted as the second argument to a generated client action.
|
|
29
|
+
*
|
|
30
|
+
* @remarks `headers` remains optional because required route headers may be
|
|
31
|
+
* supplied by `createClient({ headers })` defaults.
|
|
32
|
+
*/
|
|
33
|
+
export type RouteFetchOptions<T extends RouteSchema = any> = Omit<RequestInit, 'method' | 'body' | 'headers'> & {
|
|
48
34
|
/** Headers for this request. Undefined values are removed before `fetch`. */
|
|
49
|
-
headers?:
|
|
35
|
+
headers?: HeaderInput<T>;
|
|
50
36
|
};
|
|
37
|
+
type RouteBodyOption<T> = T extends {
|
|
38
|
+
body: RawBodySchema;
|
|
39
|
+
} ? {
|
|
40
|
+
body: BodyInit | null;
|
|
41
|
+
} : {};
|
|
42
|
+
export type RouteOptions<T extends RouteSchema = any> = RouteFetchOptions<T> & RouteBodyOption<T>;
|
|
51
43
|
export {};
|
package/dist/types/handler.d.ts
CHANGED
|
@@ -4,7 +4,10 @@ import type * as z from 'zod';
|
|
|
4
4
|
import { Promisable } from '../common.js';
|
|
5
5
|
import type { HttpAction } from '../http.js';
|
|
6
6
|
import type { InferRouteHandlerResult, InferResponseMapErrors, InferResponseMapSuccesses } from './response.js';
|
|
7
|
-
import type { RouteResponseMap, RouteSchema } from './schema.js';
|
|
7
|
+
import type { RawBodySchema, RouteResponseMap, RouteSchema } from './schema.js';
|
|
8
|
+
type InferHandlerBody<T> = T extends {
|
|
9
|
+
body: infer TBody;
|
|
10
|
+
} ? TBody extends RawBodySchema ? undefined : z.infer<TBody> : undefined;
|
|
8
11
|
type RequestContext<TMiddleware extends AnyMiddlewareChain> = MiddlewareContext<TMiddleware>;
|
|
9
12
|
/**
|
|
10
13
|
* Error response returned by `ctx.error(status, body)` in route handlers.
|
|
@@ -51,9 +54,7 @@ export type InferActionHandler<TMiddleware extends AnyMiddlewareChain, TAction e
|
|
|
51
54
|
path: TAction['schema'] extends {
|
|
52
55
|
path: any;
|
|
53
56
|
} ? z.infer<TAction['schema']['path']> : MatchParams<TPath>;
|
|
54
|
-
body: TAction['schema']
|
|
55
|
-
body: any;
|
|
56
|
-
} ? z.infer<TAction['schema']['body']> : undefined;
|
|
57
|
+
body: InferHandlerBody<TAction['schema']>;
|
|
57
58
|
headers: TAction['schema'] extends {
|
|
58
59
|
headers: any;
|
|
59
60
|
} ? z.infer<TAction['schema']['headers']> : undefined;
|
|
@@ -71,9 +72,7 @@ export type InferActionHandler<TMiddleware extends AnyMiddlewareChain, TAction e
|
|
|
71
72
|
path: TAction['schema'] extends {
|
|
72
73
|
path: any;
|
|
73
74
|
} ? z.infer<TAction['schema']['path']> : MatchParams<TPath>;
|
|
74
|
-
body: TAction['schema']
|
|
75
|
-
body: any;
|
|
76
|
-
} ? z.infer<TAction['schema']['body']> : undefined;
|
|
75
|
+
body: InferHandlerBody<TAction['schema']>;
|
|
77
76
|
headers: TAction['schema'] extends {
|
|
78
77
|
headers: any;
|
|
79
78
|
} ? z.infer<TAction['schema']['headers']> : undefined;
|
package/dist/types/schema.d.ts
CHANGED
|
@@ -36,14 +36,18 @@ export type QueryRouteSchema = {
|
|
|
36
36
|
/** Optional compile-time-only JSON or plugin response type marker. */
|
|
37
37
|
response?: RouteResponseSchema;
|
|
38
38
|
};
|
|
39
|
+
/** Marker for request bodies passed through to `fetch` without JSON encoding. */
|
|
40
|
+
export type RawBodySchema = {
|
|
41
|
+
readonly __rawBody__: unique symbol;
|
|
42
|
+
};
|
|
39
43
|
/** Schema shape for mutation route methods. */
|
|
40
44
|
export type MutationRouteSchema = {
|
|
41
45
|
/** Optional Zod object used to validate path params. */
|
|
42
46
|
path?: z.ZodObject<any>;
|
|
43
47
|
/** Mutation routes do not accept query schemas. */
|
|
44
48
|
query?: never;
|
|
45
|
-
/** Optional Zod schema used to validate the JSON request body. */
|
|
46
|
-
body?: z.
|
|
49
|
+
/** Optional Zod schema used to validate the JSON request body, or raw body marker. */
|
|
50
|
+
body?: z.ZodObject<any> | RawBodySchema;
|
|
47
51
|
/** Optional Zod object used to validate request headers. */
|
|
48
52
|
headers?: z.ZodObject<any>;
|
|
49
53
|
/** Optional compile-time-only JSON or plugin response type marker. */
|
package/dist/types/server.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AnyMiddlewareChain
|
|
1
|
+
import type { AnyMiddlewareChain } from 'alien-middleware';
|
|
2
2
|
import type { HttpAction, HttpResource, HttpRouteTree } from '../http.js';
|
|
3
3
|
import type { InferActionHandler } from './handler.js';
|
|
4
4
|
import type { Join } from './path.js';
|
|
@@ -10,6 +10,6 @@ import type { Join } from './path.js';
|
|
|
10
10
|
* Handler context is inferred from middleware plus accumulated path params,
|
|
11
11
|
* query/body schemas, and header schemas.
|
|
12
12
|
*/
|
|
13
|
-
export type RouteRequestHandlerMap<TRoutes extends HttpRouteTree = HttpRouteTree, TMiddleware extends AnyMiddlewareChain =
|
|
13
|
+
export type RouteRequestHandlerMap<TRoutes extends HttpRouteTree = HttpRouteTree, TMiddleware extends AnyMiddlewareChain = never, TPrefix extends string = ''> = {
|
|
14
14
|
[K in keyof TRoutes]: TRoutes[K] extends HttpResource<infer P, infer C> ? RouteRequestHandlerMap<C, TMiddleware, Join<TPrefix, P>> : TRoutes[K] extends HttpAction<infer P, any, any> ? InferActionHandler<TMiddleware, TRoutes[K], Join<TPrefix, P>> : never;
|
|
15
15
|
};
|
package/docs/context.md
CHANGED
|
@@ -213,6 +213,10 @@ requests with an `Origin` header.
|
|
|
213
213
|
|
|
214
214
|
`createClient({ baseURL, routes })` creates a client tree that mirrors
|
|
215
215
|
`routes`, with action functions such as `client.profiles.get(args)`.
|
|
216
|
+
Generated action functions accept a flattened first argument containing path,
|
|
217
|
+
query, and JSON body fields. Per-request `RequestInit` options, including
|
|
218
|
+
headers and abort signals, are passed as the optional second argument.
|
|
219
|
+
|
|
216
220
|
Generated action functions include:
|
|
217
221
|
|
|
218
222
|
- raw `Response` results for actions without a response schema
|
|
@@ -261,16 +265,22 @@ string-coercion step.
|
|
|
261
265
|
|
|
262
266
|
### Call client actions
|
|
263
267
|
|
|
264
|
-
Use generated client action functions for application calls
|
|
268
|
+
Use generated client action functions for application calls. The first argument
|
|
269
|
+
is a flat object containing all path, query, and JSON body fields. The optional
|
|
270
|
+
second argument is for per-request `RequestInit` options such as headers or an
|
|
271
|
+
abort signal.
|
|
265
272
|
|
|
266
273
|
```ts
|
|
267
|
-
await client.profiles.get({
|
|
268
|
-
await client.profiles.update(
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
274
|
+
await client.profiles.get({ id: '42', includePosts: true })
|
|
275
|
+
await client.profiles.update(
|
|
276
|
+
{ id: '42', name: 'Ada' },
|
|
277
|
+
{ headers: { 'x-request-id': 'docs' } }
|
|
278
|
+
)
|
|
272
279
|
```
|
|
273
280
|
|
|
281
|
+
Avoid duplicate field names across an action's path, query, and body schemas;
|
|
282
|
+
the client input is flat, so duplicate keys cannot represent separate values.
|
|
283
|
+
|
|
274
284
|
### Handle declared error responses
|
|
275
285
|
|
|
276
286
|
Use `$error<T>()` inside a response map when an error status is part of the route
|
|
@@ -308,9 +318,7 @@ const client = createClient({
|
|
|
308
318
|
routes,
|
|
309
319
|
})
|
|
310
320
|
|
|
311
|
-
const [error, user, status] = await client.getUser({
|
|
312
|
-
path: { id: 'missing' },
|
|
313
|
-
})
|
|
321
|
+
const [error, user, status] = await client.getUser({ id: 'missing' })
|
|
314
322
|
|
|
315
323
|
if (status === 404) {
|
|
316
324
|
console.log(error.message)
|
|
@@ -384,11 +392,29 @@ export const organizations = http.resource('orgs/:orgId', {
|
|
|
384
392
|
}),
|
|
385
393
|
})
|
|
386
394
|
|
|
387
|
-
await client.organizations.members.get({
|
|
388
|
-
|
|
395
|
+
await client.organizations.members.get({ orgId: 'acme', memberId: '42' })
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
### Send raw request bodies
|
|
399
|
+
|
|
400
|
+
Use `http.rawBody()` for mutation actions whose client should pass a `BodyInit`
|
|
401
|
+
through to `fetch` without JSON encoding or Zod body parsing:
|
|
402
|
+
|
|
403
|
+
```ts
|
|
404
|
+
export const uploadAvatar = http.post('profiles/:id/avatar', {
|
|
405
|
+
body: http.rawBody(),
|
|
406
|
+
headers: z.object({ 'content-type': z.string() }),
|
|
389
407
|
})
|
|
408
|
+
|
|
409
|
+
await client.uploadAvatar(
|
|
410
|
+
{ id: '42' },
|
|
411
|
+
{ body: file, headers: { 'content-type': file.type } }
|
|
412
|
+
)
|
|
390
413
|
```
|
|
391
414
|
|
|
415
|
+
Path fields still live in the flat first argument. The raw body itself is passed
|
|
416
|
+
as `body` in the second argument because it is a `RequestInit` value.
|
|
417
|
+
|
|
392
418
|
### Return custom responses
|
|
393
419
|
|
|
394
420
|
Return a `Response` from a handler for non-JSON payloads, custom status codes, or
|
|
@@ -434,7 +460,8 @@ export const profiles = http.resource('profiles/:id', {
|
|
|
434
460
|
export const routes = { profiles }
|
|
435
461
|
```
|
|
436
462
|
|
|
437
|
-
Handler maps
|
|
463
|
+
Handler maps mirror the action names, while v5 client calls use flat input
|
|
464
|
+
objects:
|
|
438
465
|
|
|
439
466
|
```ts
|
|
440
467
|
createRouter().use(routes, {
|
|
@@ -448,11 +475,8 @@ createRouter().use(routes, {
|
|
|
448
475
|
},
|
|
449
476
|
})
|
|
450
477
|
|
|
451
|
-
await client.profiles.get({
|
|
452
|
-
await client.profiles.update({
|
|
453
|
-
path: { id: '42' },
|
|
454
|
-
body: { name: 'Ada' },
|
|
455
|
-
})
|
|
478
|
+
await client.profiles.get({ id: '42' })
|
|
479
|
+
await client.profiles.update({ id: '42', name: 'Ada' })
|
|
456
480
|
```
|
|
457
481
|
|
|
458
482
|
## Patterns to prefer
|
|
@@ -480,8 +504,8 @@ await client.profiles.update({
|
|
|
480
504
|
- `$type<T>()`, `$error<T>()`, and `ndjson.$type<T>()` are compile-time-only type
|
|
481
505
|
contracts. Rouzer does not re-validate handler return values at the server
|
|
482
506
|
boundary.
|
|
483
|
-
- NDJSON support is for response streams; request bodies
|
|
484
|
-
|
|
507
|
+
- NDJSON support is for response streams; request bodies use JSON body schemas
|
|
508
|
+
unless an action declares `body: http.rawBody()`.
|
|
485
509
|
- Declared `$error<T>()` responses are JSON responses. Use a custom `Response`
|
|
486
510
|
for non-JSON error payloads.
|
|
487
511
|
- Routes that use a response plugin fail fast if the matching client or router
|
|
@@ -489,10 +513,10 @@ await client.profiles.update({
|
|
|
489
513
|
- Pathname route patterns expect an absolute client `baseURL`.
|
|
490
514
|
- Resource and action keys are API names only; paths come from the pattern
|
|
491
515
|
strings passed to `http.resource(...)` and action helpers.
|
|
492
|
-
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
for
|
|
516
|
+
- Path, query, and JSON body fields are flattened into the first client action
|
|
517
|
+
argument. Per-request `RequestInit` fields, such as `signal`, `credentials`,
|
|
518
|
+
and `headers`, belong in the second argument. `method` is reserved by Rouzer;
|
|
519
|
+
`body` is only accepted in the second argument for `http.rawBody()` actions.
|
|
496
520
|
- The HTTP action API has no `ALL` fallback route. Declare explicit actions for
|
|
497
521
|
supported methods.
|
|
498
522
|
- Rouzer does not automatically set `Access-Control-Allow-Credentials`; set it in
|
package/examples/basic-usage.ts
CHANGED
|
@@ -101,16 +101,9 @@ export async function runBasicUsageExample() {
|
|
|
101
101
|
fetch: createLocalFetch(handler),
|
|
102
102
|
})
|
|
103
103
|
|
|
104
|
-
const fetched = await client.profiles.get({
|
|
105
|
-
path: { id: '42' },
|
|
106
|
-
query: { includePosts: false },
|
|
107
|
-
headers: { 'x-request-id': 'docs' },
|
|
108
|
-
})
|
|
104
|
+
const fetched = await client.profiles.get({ id: '42', includePosts: false }, { headers: { 'x-request-id': 'docs' } })
|
|
109
105
|
|
|
110
|
-
const updated = await client.profiles.update({
|
|
111
|
-
path: { id: '42' },
|
|
112
|
-
body: { name: 'Grace' },
|
|
113
|
-
})
|
|
106
|
+
const updated = await client.profiles.update({ id: '42', name: 'Grace' })
|
|
114
107
|
|
|
115
108
|
return { fetched, updated }
|
|
116
109
|
}
|
|
@@ -89,10 +89,10 @@ export async function runErrorResponsesExample() {
|
|
|
89
89
|
fetch: createLocalFetch(handler),
|
|
90
90
|
})
|
|
91
91
|
|
|
92
|
-
const found = await client.getUser({
|
|
93
|
-
const created = await client.getUser({
|
|
94
|
-
const missing = await client.getUser({
|
|
95
|
-
const unauthorized = await client.getUser({
|
|
92
|
+
const found = await client.getUser({ id: '42' })
|
|
93
|
+
const created = await client.getUser({ id: 'created' })
|
|
94
|
+
const missing = await client.getUser({ id: 'missing' })
|
|
95
|
+
const unauthorized = await client.getUser({ id: 'unauthorized' })
|
|
96
96
|
|
|
97
97
|
return { found, created, missing, unauthorized }
|
|
98
98
|
}
|