rouzer 5.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 CHANGED
@@ -103,8 +103,10 @@ const { message } = await client.hello({
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 path,
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
 
@@ -185,7 +187,7 @@ for await (const event of await client.events()) {
185
187
 
186
188
  ## Documentation
187
189
 
188
- - [Concepts, API selection, and v2->v3 migration notes](docs/context.md)
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)
@@ -1,7 +1,8 @@
1
1
  import { Promisable } from '../common.js';
2
2
  import { type HttpAction, type HttpResource, type HttpRouteTree } from '../http.js';
3
3
  import { type ClientResponsePlugin } from '../response.js';
4
- import type { RouteInput, RouteOptions } from '../types/args.js';
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({ query: { page: 1 } })
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({ query: { page: 1 } })
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> = (...p: RouteInput<T, P> extends infer TInput ? {} extends TInput ? [input?: TInput, options?: RouteOptions<T>] : [input: TInput, options?: RouteOptions<T>] : never) => Promise<T extends {
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 {};
@@ -134,14 +134,20 @@ function connectTree(tree, prefix, plainRequest, parsedRequest) {
134
134
  const fetch = node.schema.response ? parsedRequest : plainRequest;
135
135
  return [
136
136
  key,
137
- (input, options) => fetch({
138
- schema: node.schema,
139
- path,
140
- method: node.method,
141
- input,
142
- options,
143
- $result: undefined,
144
- }),
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
+ },
145
151
  ];
146
152
  }));
147
153
  }
@@ -165,6 +171,9 @@ function validateClientResponsePlugins(tree, plugins) {
165
171
  function missingClientResponsePlugin(pluginId) {
166
172
  return new Error(`Missing client response plugin for ${pluginId}`);
167
173
  }
174
+ function hasRouteInput(node, path) {
175
+ return Boolean(node.schema.path || node.schema.query || /(^|\/)[:*]/.test(path.source));
176
+ }
168
177
  function pickObjectSchemaFields(schema, input) {
169
178
  if (typeof input !== 'object' || input === null) {
170
179
  return input;
@@ -1,4 +1,4 @@
1
- import { getResponsePluginMarkerId, responsePluginMarker, } from './response.js';
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) {
@@ -30,13 +30,14 @@ export type RouteInput<T extends RouteSchema = any, P extends string = string> =
30
30
  * @remarks `headers` remains optional because required route headers may be
31
31
  * supplied by `createClient({ headers })` defaults.
32
32
  */
33
+ export type RouteFetchOptions<T extends RouteSchema = any> = Omit<RequestInit, 'method' | 'body' | 'headers'> & {
34
+ /** Headers for this request. Undefined values are removed before `fetch`. */
35
+ headers?: HeaderInput<T>;
36
+ };
33
37
  type RouteBodyOption<T> = T extends {
34
38
  body: RawBodySchema;
35
39
  } ? {
36
40
  body: BodyInit | null;
37
41
  } : {};
38
- export type RouteOptions<T extends RouteSchema = any> = Omit<RequestInit, 'method' | 'body' | 'headers'> & RouteBodyOption<T> & {
39
- /** Headers for this request. Undefined values are removed before `fetch`. */
40
- headers?: HeaderInput<T>;
41
- };
42
+ export type RouteOptions<T extends RouteSchema = any> = RouteFetchOptions<T> & RouteBodyOption<T>;
42
43
  export {};
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({ path: { id: '42' } })
268
- await client.profiles.update({
269
- path: { id: '42' },
270
- body: { name: 'Ada' },
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
- path: { orgId: 'acme', memberId: '42' },
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 and client calls mirror the new action names:
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({ path: { id: '42' } })
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 still use the existing
484
- JSON body schema path.
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
- - Extra `RequestInit` fields in route args, such as `signal` or `credentials`,
493
- are forwarded by `createClient`; `method` and `body` are reserved for Rouzer's
494
- action metadata and validated call arguments. Use route args or client defaults
495
- for request headers.
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rouzer",
3
- "version": "5.0.0",
3
+ "version": "5.1.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {