rouzer 5.1.0 → 5.2.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 +53 -1
- package/dist/client/index.d.ts +46 -1
- package/dist/client/index.js +111 -10
- package/dist/http.d.ts +7 -1
- package/dist/http.js +7 -1
- package/dist/types/args.d.ts +4 -2
- package/docs/context.md +80 -9
- package/package.json +10 -8
package/README.md
CHANGED
|
@@ -14,6 +14,7 @@ that contract to:
|
|
|
14
14
|
- match and validate server requests before handlers run
|
|
15
15
|
- type handler context from path, query/body, headers, and middleware
|
|
16
16
|
- attach typed client action functions such as `client.profiles.get(...)`
|
|
17
|
+
- send JSON object request bodies or raw `BodyInit` payloads
|
|
17
18
|
- parse typed JSON responses, declared error responses, and NDJSON streams
|
|
18
19
|
|
|
19
20
|
Rouzer optimizes for shared TypeScript route modules over language-agnostic API
|
|
@@ -106,7 +107,8 @@ const { message } = await client.hello({
|
|
|
106
107
|
validate flat route arguments before `fetch`; server handlers validate matched
|
|
107
108
|
path, query, headers, and JSON bodies before your handler runs. Per-request
|
|
108
109
|
headers, abort signals, and other `RequestInit` options are passed as a second
|
|
109
|
-
client action argument.
|
|
110
|
+
client action argument. Routes declared with `body: http.rawBody()` pass a
|
|
111
|
+
`BodyInit` payload through to `fetch` without JSON encoding.
|
|
110
112
|
|
|
111
113
|
### Typed status responses
|
|
112
114
|
|
|
@@ -151,6 +153,56 @@ const [error, user, status] = await client.getUser({ id: '42' })
|
|
|
151
153
|
Success entries resolve as `[null, value, status]`; declared error entries
|
|
152
154
|
resolve as `[error, null, status]`.
|
|
153
155
|
|
|
156
|
+
### Raw request bodies
|
|
157
|
+
|
|
158
|
+
Use `http.rawBody()` when an action needs to send a `BodyInit` payload such as a
|
|
159
|
+
`Blob`, `Uint8Array`, `ReadableStream`, `FormData`, or string without JSON
|
|
160
|
+
encoding.
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
export const uploadAvatar = http.post('profiles/:id/avatar', {
|
|
164
|
+
body: http.rawBody(),
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
await client.uploadAvatar({ id: '42' }, { body: file })
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
For raw-body routes without path or query input, the generated client accepts the
|
|
171
|
+
body as the first argument:
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
export const upload = http.post('upload', {
|
|
175
|
+
body: http.rawBody(),
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
await client.upload(file, { headers: { 'content-type': file.type } })
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Server handlers for raw-body routes read from `ctx.request` directly with Fetch
|
|
182
|
+
APIs such as `arrayBuffer()`, `blob()`, `formData()`, or `text()`.
|
|
183
|
+
|
|
184
|
+
### Client lifecycle hooks
|
|
185
|
+
|
|
186
|
+
Pass `clientHook` to observe generated client action calls without wrapping the
|
|
187
|
+
client tree:
|
|
188
|
+
|
|
189
|
+
```ts
|
|
190
|
+
const client = createClient({
|
|
191
|
+
baseURL: 'https://example.com/api/',
|
|
192
|
+
routes,
|
|
193
|
+
clientHook(event) {
|
|
194
|
+
if (event.type === 'request.success') {
|
|
195
|
+
console.log(event.routeName, event.durationMs)
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
})
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Rouzer emits `request.start` before client-side validation, then
|
|
202
|
+
`request.success` when the action resolves or `request.error` when it rejects.
|
|
203
|
+
Terminal events include the parsed response or thrown error plus `durationMs`.
|
|
204
|
+
Hook errors are swallowed.
|
|
205
|
+
|
|
154
206
|
### NDJSON response streams
|
|
155
207
|
|
|
156
208
|
Use `response: ndjson.$type<T>()` for endpoints that stream
|
package/dist/client/index.d.ts
CHANGED
|
@@ -5,6 +5,37 @@ import type { RouteFetchOptions, RouteInput, RouteOptions } from '../types/args.
|
|
|
5
5
|
import type { RawBodySchema } from '../types/schema.js';
|
|
6
6
|
import type { InferRouteResponse } from '../types/response.js';
|
|
7
7
|
import type { RouteSchema } from '../types/schema.js';
|
|
8
|
+
/** Lifecycle event emitted by generated client action functions. */
|
|
9
|
+
export type RouzerClientHookEvent = {
|
|
10
|
+
type: 'request.start';
|
|
11
|
+
opId: string;
|
|
12
|
+
routeName: string;
|
|
13
|
+
method: string;
|
|
14
|
+
pathPattern: string;
|
|
15
|
+
payload: unknown;
|
|
16
|
+
} | {
|
|
17
|
+
type: 'request.success';
|
|
18
|
+
opId: string;
|
|
19
|
+
routeName: string;
|
|
20
|
+
method: string;
|
|
21
|
+
pathPattern: string;
|
|
22
|
+
payload: unknown;
|
|
23
|
+
response: unknown;
|
|
24
|
+
status?: number;
|
|
25
|
+
durationMs: number;
|
|
26
|
+
} | {
|
|
27
|
+
type: 'request.error';
|
|
28
|
+
opId: string;
|
|
29
|
+
routeName: string;
|
|
30
|
+
method: string;
|
|
31
|
+
pathPattern: string;
|
|
32
|
+
payload: unknown;
|
|
33
|
+
error: unknown;
|
|
34
|
+
status?: number;
|
|
35
|
+
durationMs: number;
|
|
36
|
+
};
|
|
37
|
+
/** Best-effort observer for generated client action lifecycles. */
|
|
38
|
+
export type RouzerClientHook = (event: RouzerClientHookEvent) => void;
|
|
8
39
|
/** Client type inferred from an HTTP route tree passed to `createClient`. */
|
|
9
40
|
export type RouzerClient<TRoutes extends HttpRouteTree = Record<string, never>> = ReturnType<typeof createClient<TRoutes>>;
|
|
10
41
|
/**
|
|
@@ -51,6 +82,12 @@ export declare function createClient<TRoutes extends HttpRouteTree = Record<stri
|
|
|
51
82
|
onJsonError?: (response: Response) => Promisable<unknown>;
|
|
52
83
|
/** Custom `fetch` implementation to use for requests. */
|
|
53
84
|
fetch?: typeof globalThis.fetch;
|
|
85
|
+
/**
|
|
86
|
+
* Best-effort lifecycle observer for generated client action calls.
|
|
87
|
+
*
|
|
88
|
+
* @remarks Hook errors are swallowed and never change request behavior.
|
|
89
|
+
*/
|
|
90
|
+
clientHook?: RouzerClientHook;
|
|
54
91
|
}): ClientTree<TRoutes, ""> & {
|
|
55
92
|
clientConfig: {
|
|
56
93
|
/**
|
|
@@ -90,6 +127,12 @@ export declare function createClient<TRoutes extends HttpRouteTree = Record<stri
|
|
|
90
127
|
onJsonError?: (response: Response) => Promisable<unknown>;
|
|
91
128
|
/** Custom `fetch` implementation to use for requests. */
|
|
92
129
|
fetch?: typeof globalThis.fetch;
|
|
130
|
+
/**
|
|
131
|
+
* Best-effort lifecycle observer for generated client action calls.
|
|
132
|
+
*
|
|
133
|
+
* @remarks Hook errors are swallowed and never change request behavior.
|
|
134
|
+
*/
|
|
135
|
+
clientHook?: RouzerClientHook;
|
|
93
136
|
};
|
|
94
137
|
};
|
|
95
138
|
type Join<A extends string, B extends string> = A extends '' ? B : B extends '' ? A : `${A}/${B}`;
|
|
@@ -105,7 +148,9 @@ export type ClientTree<T extends HttpRouteTree, TPrefix extends string = ''> = {
|
|
|
105
148
|
* union of `[null, value, status]` success entries and `[error, null, status]`
|
|
106
149
|
* error entries. Actions whose schema has a plugin response marker return the
|
|
107
150
|
* plugin's client result type. Actions without a response marker return the raw
|
|
108
|
-
* `Response`.
|
|
151
|
+
* `Response`. Raw-body actions with no path or query input accept
|
|
152
|
+
* `(body, options)`; raw-body actions with route input accept
|
|
153
|
+
* `(input, { body, ...options })`.
|
|
109
154
|
*/
|
|
110
155
|
export type RouteFunction<T extends RouteSchema, P extends string> = T extends {
|
|
111
156
|
body: RawBodySchema;
|
package/dist/client/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import { shake } from '../common.js';
|
|
|
4
4
|
import { isRawBodySchema, } from '../http.js';
|
|
5
5
|
import { getResponseMapPluginIds, isErrorMarker, isResponseMap, } from '../response-map.js';
|
|
6
6
|
import { createResponsePluginMap, getResponsePluginMarkerId, } from '../response.js';
|
|
7
|
+
let nextClientOpId = 0;
|
|
7
8
|
/**
|
|
8
9
|
* Create a typed fetch client for an HTTP route tree.
|
|
9
10
|
*
|
|
@@ -16,7 +17,10 @@ export function createClient(config) {
|
|
|
16
17
|
const fetch = config.fetch ?? globalThis.fetch;
|
|
17
18
|
const responsePlugins = createResponsePluginMap(config.plugins, 'client response');
|
|
18
19
|
validateClientResponsePlugins(config.routes, responsePlugins);
|
|
19
|
-
async function plainRequest(
|
|
20
|
+
async function plainRequest(props) {
|
|
21
|
+
const { path: pathPattern, method, input = {}, schema } = props;
|
|
22
|
+
const { body: rawBody, ...options } = props.options ?? {};
|
|
23
|
+
let { headers, ...init } = options;
|
|
20
24
|
const path = schema.path
|
|
21
25
|
? schema.path.parse(pickObjectSchemaFields(schema.path, input))
|
|
22
26
|
: input;
|
|
@@ -48,7 +52,7 @@ export function createClient(config) {
|
|
|
48
52
|
if (schema.headers) {
|
|
49
53
|
headers = schema.headers.parse(headers);
|
|
50
54
|
}
|
|
51
|
-
return fetch(url, {
|
|
55
|
+
return (await fetch(url, {
|
|
52
56
|
...init,
|
|
53
57
|
method,
|
|
54
58
|
body: isRawBodySchema(schema.body)
|
|
@@ -57,10 +61,9 @@ export function createClient(config) {
|
|
|
57
61
|
? JSON.stringify(body)
|
|
58
62
|
: undefined,
|
|
59
63
|
headers: (headers ?? defaultHeaders),
|
|
60
|
-
});
|
|
64
|
+
}));
|
|
61
65
|
}
|
|
62
|
-
async function
|
|
63
|
-
const response = await plainRequest(props);
|
|
66
|
+
async function parseResponse(response, props) {
|
|
64
67
|
const responseSchema = props.schema.response;
|
|
65
68
|
// Handle status-keyed response maps
|
|
66
69
|
if (isResponseMap(responseSchema)) {
|
|
@@ -106,6 +109,25 @@ export function createClient(config) {
|
|
|
106
109
|
}
|
|
107
110
|
return response.json();
|
|
108
111
|
}
|
|
112
|
+
async function plainClientRequest(props) {
|
|
113
|
+
const response = await plainRequest(props);
|
|
114
|
+
return {
|
|
115
|
+
value: response,
|
|
116
|
+
status: response.status,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
async function parsedClientRequest(props) {
|
|
120
|
+
const response = await plainRequest(props);
|
|
121
|
+
try {
|
|
122
|
+
return {
|
|
123
|
+
value: await parseResponse(response, props),
|
|
124
|
+
status: response.status,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
throw new ClientRequestFailure(error, response.status);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
109
131
|
async function handleResponseError(response, props) {
|
|
110
132
|
if (config.onJsonError) {
|
|
111
133
|
return config.onJsonError(response);
|
|
@@ -118,39 +140,118 @@ export function createClient(config) {
|
|
|
118
140
|
throw error;
|
|
119
141
|
}
|
|
120
142
|
return {
|
|
121
|
-
...connectTree(config.routes, '',
|
|
143
|
+
...connectTree(config.routes, '', '', plainClientRequest, parsedClientRequest, config.clientHook),
|
|
122
144
|
clientConfig: config,
|
|
123
145
|
};
|
|
124
146
|
}
|
|
125
|
-
|
|
147
|
+
class ClientRequestFailure {
|
|
148
|
+
error;
|
|
149
|
+
status;
|
|
150
|
+
constructor(error, status) {
|
|
151
|
+
this.error = error;
|
|
152
|
+
this.status = status;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
function connectTree(tree, prefix, namePrefix, plainRequest, parsedRequest, clientHook) {
|
|
126
156
|
return Object.fromEntries(Object.entries(tree).map(([key, node]) => {
|
|
127
157
|
if (node.kind === 'resource') {
|
|
128
158
|
return [
|
|
129
159
|
key,
|
|
130
|
-
connectTree(node.children, joinPaths(prefix, node.path.source), plainRequest, parsedRequest),
|
|
160
|
+
connectTree(node.children, joinPaths(prefix, node.path.source), joinNames(namePrefix, key), plainRequest, parsedRequest, clientHook),
|
|
131
161
|
];
|
|
132
162
|
}
|
|
133
163
|
const path = RoutePattern.parse(joinPaths(prefix, node.path?.source ?? ''));
|
|
134
164
|
const fetch = node.schema.response ? parsedRequest : plainRequest;
|
|
165
|
+
const routeName = joinNames(namePrefix, key);
|
|
135
166
|
return [
|
|
136
167
|
key,
|
|
137
168
|
(input, options) => {
|
|
169
|
+
const payload = input;
|
|
138
170
|
if (isRawBodySchema(node.schema.body) && !hasRouteInput(node, path)) {
|
|
139
171
|
options = { ...options, body: input };
|
|
140
172
|
input = undefined;
|
|
141
173
|
}
|
|
142
|
-
return
|
|
174
|
+
return runClientRequest({
|
|
143
175
|
schema: node.schema,
|
|
144
176
|
path,
|
|
177
|
+
routeName,
|
|
145
178
|
method: node.method,
|
|
146
179
|
input,
|
|
180
|
+
payload,
|
|
147
181
|
options,
|
|
148
182
|
$result: undefined,
|
|
149
|
-
});
|
|
183
|
+
}, fetch, clientHook);
|
|
150
184
|
},
|
|
151
185
|
];
|
|
152
186
|
}));
|
|
153
187
|
}
|
|
188
|
+
async function runClientRequest(request, fetch, clientHook) {
|
|
189
|
+
if (!clientHook) {
|
|
190
|
+
try {
|
|
191
|
+
return (await fetch(request)).value;
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
throw getClientRequestFailure(error)?.error ?? error;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const opId = createClientOpId();
|
|
198
|
+
const startTime = Date.now();
|
|
199
|
+
const baseEvent = {
|
|
200
|
+
opId,
|
|
201
|
+
routeName: request.routeName,
|
|
202
|
+
method: request.method,
|
|
203
|
+
pathPattern: request.path.source,
|
|
204
|
+
payload: request.payload,
|
|
205
|
+
};
|
|
206
|
+
emitClientHook(clientHook, {
|
|
207
|
+
type: 'request.start',
|
|
208
|
+
...baseEvent,
|
|
209
|
+
});
|
|
210
|
+
try {
|
|
211
|
+
const result = await fetch(request);
|
|
212
|
+
emitClientHook(clientHook, {
|
|
213
|
+
type: 'request.success',
|
|
214
|
+
...baseEvent,
|
|
215
|
+
response: result.value,
|
|
216
|
+
...clientRequestStatus(result.status),
|
|
217
|
+
durationMs: Date.now() - startTime,
|
|
218
|
+
});
|
|
219
|
+
return result.value;
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
const failure = getClientRequestFailure(error);
|
|
223
|
+
const eventError = failure ? failure.error : error;
|
|
224
|
+
emitClientHook(clientHook, {
|
|
225
|
+
type: 'request.error',
|
|
226
|
+
...baseEvent,
|
|
227
|
+
error: eventError,
|
|
228
|
+
...clientRequestStatus(failure?.status),
|
|
229
|
+
durationMs: Date.now() - startTime,
|
|
230
|
+
});
|
|
231
|
+
throw eventError;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
function emitClientHook(clientHook, event) {
|
|
235
|
+
try {
|
|
236
|
+
clientHook(event);
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
// Lifecycle hooks are observability-only and must not affect requests.
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
function createClientOpId() {
|
|
243
|
+
nextClientOpId += 1;
|
|
244
|
+
return `rouzer:${Date.now().toString(36)}:${nextClientOpId.toString(36)}`;
|
|
245
|
+
}
|
|
246
|
+
function getClientRequestFailure(error) {
|
|
247
|
+
return error instanceof ClientRequestFailure ? error : undefined;
|
|
248
|
+
}
|
|
249
|
+
function clientRequestStatus(status) {
|
|
250
|
+
return status === undefined ? {} : { status };
|
|
251
|
+
}
|
|
252
|
+
function joinNames(left, right) {
|
|
253
|
+
return [left, right].filter(Boolean).join('.');
|
|
254
|
+
}
|
|
154
255
|
function validateClientResponsePlugins(tree, plugins) {
|
|
155
256
|
for (const node of Object.values(tree)) {
|
|
156
257
|
if (node.kind === 'resource') {
|
package/dist/http.d.ts
CHANGED
|
@@ -62,6 +62,12 @@ 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
|
-
/**
|
|
65
|
+
/**
|
|
66
|
+
* Declare a request body that is passed through to `fetch` without JSON encoding.
|
|
67
|
+
*
|
|
68
|
+
* @remarks For routes with path or query input, pass the body as
|
|
69
|
+
* `options.body`. For raw-body routes without input, generated client actions
|
|
70
|
+
* accept the body as their first argument.
|
|
71
|
+
*/
|
|
66
72
|
export declare function rawBody(): RawBodySchema;
|
|
67
73
|
export declare function isRawBodySchema(schema: unknown): schema is RawBodySchema;
|
package/dist/http.js
CHANGED
|
@@ -29,7 +29,13 @@ function deleteAction(pathOrSchema, schema) {
|
|
|
29
29
|
return action('DELETE', pathOrSchema, schema);
|
|
30
30
|
}
|
|
31
31
|
export { deleteAction as delete };
|
|
32
|
-
/**
|
|
32
|
+
/**
|
|
33
|
+
* Declare a request body that is passed through to `fetch` without JSON encoding.
|
|
34
|
+
*
|
|
35
|
+
* @remarks For routes with path or query input, pass the body as
|
|
36
|
+
* `options.body`. For raw-body routes without input, generated client actions
|
|
37
|
+
* accept the body as their first argument.
|
|
38
|
+
*/
|
|
33
39
|
export function rawBody() {
|
|
34
40
|
return { __rawBody__: Symbol('rouzer.rawBody') };
|
|
35
41
|
}
|
package/dist/types/args.d.ts
CHANGED
|
@@ -25,10 +25,12 @@ type HeaderInput<T> = T extends {
|
|
|
25
25
|
*/
|
|
26
26
|
export type RouteInput<T extends RouteSchema = any, P extends string = string> = [T] extends [Any] ? any : PathInput<T, P> & QueryInput<T> & BodyInput<T>;
|
|
27
27
|
/**
|
|
28
|
-
* Fetch options accepted
|
|
28
|
+
* Fetch options accepted by a generated client action.
|
|
29
29
|
*
|
|
30
30
|
* @remarks `headers` remains optional because required route headers may be
|
|
31
|
-
* supplied by `createClient({ headers })` defaults.
|
|
31
|
+
* supplied by `createClient({ headers })` defaults. Raw-body routes with path or
|
|
32
|
+
* query input accept `body` here; raw-body routes without input accept the body
|
|
33
|
+
* as the first client action argument and these options as the second.
|
|
32
34
|
*/
|
|
33
35
|
export type RouteFetchOptions<T extends RouteSchema = any> = Omit<RequestInit, 'method' | 'body' | 'headers'> & {
|
|
34
36
|
/** Headers for this request. Undefined values are removed before `fetch`. */
|
package/docs/context.md
CHANGED
|
@@ -85,7 +85,7 @@ Method schemas describe the request pieces Rouzer should validate:
|
|
|
85
85
|
| Action helper | Request schemas | Notes |
|
|
86
86
|
| --------------------------------- | -------------------------------------- | ---------------- |
|
|
87
87
|
| `http.get(...)` | `path`, `query`, `headers`, `response` | No request body. |
|
|
88
|
-
| `http.post/put/patch/delete(...)` | `path`, `body`, `headers`, `response` | No query schema. |
|
|
88
|
+
| `http.post/put/patch/delete(...)` | `path`, `body`, `headers`, `response` | No query schema. `body` is a Zod object for JSON or `http.rawBody()` for pass-through payloads. |
|
|
89
89
|
|
|
90
90
|
If you omit a `path` schema, TypeScript infers path params from the pattern and
|
|
91
91
|
server handlers receive them as strings. Add a Zod `path` schema when you need
|
|
@@ -215,7 +215,9 @@ requests with an `Origin` header.
|
|
|
215
215
|
`routes`, with action functions such as `client.profiles.get(args)`.
|
|
216
216
|
Generated action functions accept a flattened first argument containing path,
|
|
217
217
|
query, and JSON body fields. Per-request `RequestInit` options, including
|
|
218
|
-
headers and abort signals, are passed as the optional second argument.
|
|
218
|
+
headers and abort signals, are passed as the optional second argument. For
|
|
219
|
+
`http.rawBody()` routes, the raw `BodyInit` payload is passed through to `fetch`
|
|
220
|
+
without JSON encoding.
|
|
219
221
|
|
|
220
222
|
Generated action functions include:
|
|
221
223
|
|
|
@@ -240,6 +242,54 @@ top, and a custom `fetch` implementation can be supplied for tests or non-browse
|
|
|
240
242
|
runtimes. The returned client exposes the original options as `clientConfig`, so
|
|
241
243
|
route actions named `config` remain available as `client.config(...)`.
|
|
242
244
|
|
|
245
|
+
### Client lifecycle hooks
|
|
246
|
+
|
|
247
|
+
`createClient({ clientHook })` observes generated client action calls without
|
|
248
|
+
wrapping the returned client tree:
|
|
249
|
+
|
|
250
|
+
```ts
|
|
251
|
+
const client = createClient({
|
|
252
|
+
baseURL: 'https://example.com/api/',
|
|
253
|
+
routes,
|
|
254
|
+
clientHook(event) {
|
|
255
|
+
if (event.type === 'request.success') {
|
|
256
|
+
console.log({
|
|
257
|
+
opId: event.opId,
|
|
258
|
+
routeName: event.routeName,
|
|
259
|
+
durationMs: event.durationMs,
|
|
260
|
+
})
|
|
261
|
+
}
|
|
262
|
+
},
|
|
263
|
+
})
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
Rouzer emits:
|
|
267
|
+
|
|
268
|
+
- `request.start` before client-side validation
|
|
269
|
+
- `request.success` when the generated action resolves
|
|
270
|
+
- `request.error` when the generated action rejects
|
|
271
|
+
|
|
272
|
+
Each event includes an opaque per-call `opId`, the generated client route name
|
|
273
|
+
such as `session.create`, the HTTP method, the joined route path pattern, and
|
|
274
|
+
the generated action's first argument as `payload`. Terminal events include
|
|
275
|
+
`durationMs`, either the resolved `response` or thrown `error`, and `status` when
|
|
276
|
+
an HTTP response was received.
|
|
277
|
+
|
|
278
|
+
`request.error` covers client validation failures, transport failures,
|
|
279
|
+
undeclared HTTP errors, JSON parsing failures, response plugin decode failures,
|
|
280
|
+
and rejected `onJsonError` handlers. A declared error response returned as data
|
|
281
|
+
from a response map is a successful generated action and emits
|
|
282
|
+
`request.success`.
|
|
283
|
+
|
|
284
|
+
Lifecycle hooks are best-effort observability only. If `clientHook` throws,
|
|
285
|
+
Rouzer swallows that hook error and preserves the generated action's original
|
|
286
|
+
behavior.
|
|
287
|
+
|
|
288
|
+
For response plugins that return streams, such as NDJSON, `request.success` is
|
|
289
|
+
emitted when the generated action resolves to the stream object. Errors that
|
|
290
|
+
happen later while the caller consumes the stream are outside the first lifecycle
|
|
291
|
+
hook surface.
|
|
292
|
+
|
|
243
293
|
## Lifecycle
|
|
244
294
|
|
|
245
295
|
1. Define shared HTTP actions/resources with `rouzer/http` and Zod schemas.
|
|
@@ -248,8 +298,8 @@ route actions named `config` remain available as `client.config(...)`.
|
|
|
248
298
|
are needed.
|
|
249
299
|
3. Create a client with the same route tree, plus matching client response
|
|
250
300
|
plugins when needed.
|
|
251
|
-
4. Client action calls validate `path`, `query`, `body`, and
|
|
252
|
-
`fetch`.
|
|
301
|
+
4. Client action calls validate `path`, `query`, JSON object `body`, and
|
|
302
|
+
`headers` before `fetch`. Raw bodies are passed through without validation.
|
|
253
303
|
5. The router matches the request, validates the matched inputs, and calls the
|
|
254
304
|
handler.
|
|
255
305
|
6. Plain handler results become JSON responses, response-map helpers choose
|
|
@@ -259,7 +309,8 @@ route actions named `config` remain available as `client.config(...)`.
|
|
|
259
309
|
On the server, `path`, `query`, and `headers` values originate as strings. Rouzer
|
|
260
310
|
coerces Zod `number` schemas with `Number(value)` and Zod `boolean` schemas from
|
|
261
311
|
`"true"` and `"false"`. JSON request bodies are parsed and validated without that
|
|
262
|
-
string-coercion step.
|
|
312
|
+
string-coercion step. Raw request bodies declared with `http.rawBody()` are not
|
|
313
|
+
parsed by Rouzer.
|
|
263
314
|
|
|
264
315
|
## Common tasks
|
|
265
316
|
|
|
@@ -412,8 +463,26 @@ await client.uploadAvatar(
|
|
|
412
463
|
)
|
|
413
464
|
```
|
|
414
465
|
|
|
415
|
-
|
|
416
|
-
|
|
466
|
+
When a raw-body route has path or query input, path/query fields still live in
|
|
467
|
+
the flat first argument. The raw body itself is passed as `body` in the second
|
|
468
|
+
argument because it is a `RequestInit` value.
|
|
469
|
+
|
|
470
|
+
For raw-body routes without path or query input, the generated client action
|
|
471
|
+
accepts the body as the first argument and fetch options as the second:
|
|
472
|
+
|
|
473
|
+
```ts
|
|
474
|
+
export const upload = http.post('uploads', {
|
|
475
|
+
body: http.rawBody(),
|
|
476
|
+
})
|
|
477
|
+
|
|
478
|
+
await client.upload(file, {
|
|
479
|
+
headers: { 'content-type': file.type },
|
|
480
|
+
})
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
Server handlers for raw-body routes read from `ctx.request` directly with Fetch
|
|
484
|
+
APIs such as `arrayBuffer()`, `blob()`, `formData()`, or `text()`. Rouzer does
|
|
485
|
+
not parse or validate raw request bodies.
|
|
417
486
|
|
|
418
487
|
### Return custom responses
|
|
419
488
|
|
|
@@ -515,8 +584,10 @@ await client.profiles.update({ id: '42', name: 'Ada' })
|
|
|
515
584
|
strings passed to `http.resource(...)` and action helpers.
|
|
516
585
|
- Path, query, and JSON body fields are flattened into the first client action
|
|
517
586
|
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
|
|
587
|
+
and `headers`, belong in the second argument. `method` is reserved by Rouzer.
|
|
588
|
+
For `http.rawBody()` actions, `body` is accepted in the second argument when
|
|
589
|
+
the route has path or query input; raw-body actions without route input accept
|
|
590
|
+
the body as the first argument.
|
|
520
591
|
- The HTTP action API has no `ALL` fallback route. Declare explicit actions for
|
|
521
592
|
supported methods.
|
|
522
593
|
- Rouzer does not automatically set `Access-Control-Allow-Credentials`; set it in
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rouzer",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.2.0",
|
|
4
|
+
"packageManager": "pnpm@11.5.1",
|
|
4
5
|
"type": "module",
|
|
5
6
|
"exports": {
|
|
6
7
|
".": {
|
|
@@ -16,6 +17,12 @@
|
|
|
16
17
|
"import": "./dist/ndjson.js"
|
|
17
18
|
}
|
|
18
19
|
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "rm -rf dist && tsgo -b tsconfig.json",
|
|
22
|
+
"format": "prettier --write src test",
|
|
23
|
+
"test": "vitest run",
|
|
24
|
+
"prepublishOnly": "pnpm build"
|
|
25
|
+
},
|
|
19
26
|
"peerDependencies": {
|
|
20
27
|
"zod": ">=4"
|
|
21
28
|
},
|
|
@@ -49,10 +56,5 @@
|
|
|
49
56
|
"examples",
|
|
50
57
|
"README.md",
|
|
51
58
|
"!*.tsbuildinfo"
|
|
52
|
-
]
|
|
53
|
-
|
|
54
|
-
"build": "rm -rf dist && tsgo -b tsconfig.json",
|
|
55
|
-
"format": "prettier --write src test",
|
|
56
|
-
"test": "vitest run"
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
+
]
|
|
60
|
+
}
|