rouzer 3.0.2 → 3.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 +90 -6
- package/dist/client/index.d.ts +22 -9
- package/dist/client/index.js +88 -12
- package/dist/common.d.ts +11 -1
- package/dist/http.js +3 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/ndjson.d.ts +54 -0
- package/dist/ndjson.js +161 -0
- package/dist/response-map.d.ts +10 -0
- package/dist/response-map.js +32 -0
- package/dist/response.d.ts +49 -0
- package/dist/response.js +33 -0
- package/dist/server/router.d.ts +5 -2
- package/dist/server/router.js +81 -5
- package/dist/type.d.ts +33 -4
- package/dist/type.js +32 -3
- package/dist/types/handler.d.ts +56 -6
- package/dist/types/request.d.ts +1 -1
- package/dist/types/response.d.ts +52 -4
- package/dist/types/schema.d.ts +23 -7
- package/docs/context.md +220 -25
- package/examples/error-responses.ts +98 -0
- package/examples/ndjson-stream.ts +68 -0
- package/package.json +6 -1
package/docs/context.md
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Rouzer is for applications that want one TypeScript HTTP route tree to drive
|
|
4
4
|
both the server and the client that calls it. A route tree combines URL
|
|
5
|
-
patterns, named actions, HTTP method schemas, and optional compile-time
|
|
6
|
-
types.
|
|
5
|
+
patterns, named actions, HTTP method schemas, and optional compile-time success,
|
|
6
|
+
error, or plugin response types.
|
|
7
7
|
|
|
8
8
|
## When to use Rouzer
|
|
9
9
|
|
|
@@ -17,9 +17,12 @@ Use Rouzer when:
|
|
|
17
17
|
- generated clients should stay close to route definitions instead of being
|
|
18
18
|
produced by a separate OpenAPI build step
|
|
19
19
|
|
|
20
|
-
Rouzer is not a response
|
|
21
|
-
server framework. It focuses on typed route contracts, validation,
|
|
22
|
-
small client wrapper.
|
|
20
|
+
Rouzer is not a server response validator, an OpenAPI generator, or a complete
|
|
21
|
+
server framework. It focuses on typed route contracts, request validation,
|
|
22
|
+
routing, and a small client wrapper. Response markers are type contracts; if
|
|
23
|
+
response data comes from an untrusted source, validate it where it enters your
|
|
24
|
+
server or client code instead of relying on the router to re-check handler
|
|
25
|
+
returns.
|
|
23
26
|
|
|
24
27
|
## Core abstractions
|
|
25
28
|
|
|
@@ -79,9 +82,9 @@ them out of resource/base-path composition.
|
|
|
79
82
|
|
|
80
83
|
Method schemas describe the request pieces Rouzer should validate:
|
|
81
84
|
|
|
82
|
-
| Action helper
|
|
83
|
-
|
|
|
84
|
-
| `http.get(...)`
|
|
85
|
+
| Action helper | Request schemas | Notes |
|
|
86
|
+
| --------------------------------- | -------------------------------------- | ---------------- |
|
|
87
|
+
| `http.get(...)` | `path`, `query`, `headers`, `response` | No request body. |
|
|
85
88
|
| `http.post/put/patch/delete(...)` | `path`, `body`, `headers`, `response` | No query schema. |
|
|
86
89
|
|
|
87
90
|
If you omit a `path` schema, TypeScript infers path params from the pattern and
|
|
@@ -92,15 +95,76 @@ The HTTP action API models explicit operations. It does not expose the old
|
|
|
92
95
|
method-map `ALL` fallback route shape; declare the concrete methods your client
|
|
93
96
|
and server support.
|
|
94
97
|
|
|
95
|
-
###
|
|
98
|
+
### Response markers and maps
|
|
96
99
|
|
|
97
|
-
`response: $type<T>()` is a TypeScript-only marker
|
|
98
|
-
action functions what
|
|
99
|
-
validate
|
|
100
|
+
`response: $type<T>()` is a TypeScript-only marker for JSON success payloads. It
|
|
101
|
+
tells handlers and client action functions what payload type to expect, but
|
|
102
|
+
Rouzer does not validate handler return values at the server boundary. Validate
|
|
103
|
+
response data where it enters your system, such as an external API client,
|
|
104
|
+
database decoder, or UI/client boundary, when runtime integrity is required.
|
|
105
|
+
|
|
106
|
+
Use a status-keyed response map when callers need to branch on declared statuses:
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
import { $error, $type } from 'rouzer'
|
|
110
|
+
import * as http from 'rouzer/http'
|
|
111
|
+
|
|
112
|
+
type User = { id: string; name: string }
|
|
113
|
+
type NotFound = { code: 'NOT_FOUND'; message: string }
|
|
114
|
+
|
|
115
|
+
export const getUser = http.get('users/:id', {
|
|
116
|
+
response: {
|
|
117
|
+
200: $type<User>(),
|
|
118
|
+
201: $type<User>(),
|
|
119
|
+
404: $error<NotFound>(),
|
|
120
|
+
},
|
|
121
|
+
})
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Success entries use `$type<T>()` or a response plugin marker. Error entries use
|
|
125
|
+
`$error<T>()` and are encoded as JSON. Generated client action functions resolve
|
|
126
|
+
declared statuses as tuples:
|
|
127
|
+
|
|
128
|
+
- success: `[null, value, status]`
|
|
129
|
+
- error: `[error, null, status]`
|
|
130
|
+
|
|
131
|
+
Declared error statuses do not reject the client promise. Undeclared statuses
|
|
132
|
+
still go through `onJsonError` or throw the default error.
|
|
133
|
+
|
|
134
|
+
Handlers for response-map actions may return the default success value directly,
|
|
135
|
+
use `ctx.success(status, body)` to choose a declared success status, or use
|
|
136
|
+
`ctx.error(status, body)` to return a declared error status. The `ctx.error` and
|
|
137
|
+
`ctx.success` helpers only accept statuses and bodies declared in the response
|
|
138
|
+
map.
|
|
139
|
+
|
|
140
|
+
`response: ndjson.$type<T>()` is a TypeScript-only marker for newline-delimited
|
|
141
|
+
JSON response streams from the `rouzer/ndjson` subpath. Register
|
|
142
|
+
`ndjson.routerPlugin` with `createRouter(...)` and `ndjson.clientPlugin` with
|
|
143
|
+
`createClient(...)` for routes that use this marker. Handlers return an
|
|
144
|
+
`Iterable<T>` or `AsyncIterable<T>`; Rouzer serializes each item as one JSON line
|
|
145
|
+
and sets the response content type to `application/x-ndjson; charset=utf-8`.
|
|
146
|
+
Client action functions resolve to an `AsyncIterable<T>` parsed from the
|
|
147
|
+
response body. Streamed items are parsed as JSON but are not validated against a
|
|
148
|
+
Zod schema.
|
|
100
149
|
|
|
101
150
|
Actions without a `response` marker return a raw `Response` from client action
|
|
102
|
-
functions. Actions with
|
|
103
|
-
|
|
151
|
+
functions. Actions with `response: $type<T>()` return parsed JSON typed as `T`.
|
|
152
|
+
Actions with a response map return the tuple union described by that map.
|
|
153
|
+
|
|
154
|
+
### Response plugins
|
|
155
|
+
|
|
156
|
+
Response plugins add non-JSON response codecs without changing route matching or
|
|
157
|
+
request validation. A plugin package provides a compile-time response marker and
|
|
158
|
+
matching runtime plugins. For NDJSON, those are `ndjson.$type<T>()`,
|
|
159
|
+
`ndjson.routerPlugin`, and `ndjson.clientPlugin`.
|
|
160
|
+
|
|
161
|
+
The router plugin encodes non-`Response` handler results into an HTTP `Response`.
|
|
162
|
+
The client plugin decodes successful HTTP responses for generated client action
|
|
163
|
+
functions. Plugin markers can also be success entries in a status-keyed response
|
|
164
|
+
map. Rouzer validates plugin registration when routes are attached to a router or
|
|
165
|
+
client, so routes that use an unregistered response marker fail fast instead of
|
|
166
|
+
falling back to JSON. Response plugins do not automatically validate response
|
|
167
|
+
payloads unless the plugin itself implements validation.
|
|
104
168
|
|
|
105
169
|
### Router
|
|
106
170
|
|
|
@@ -133,7 +197,12 @@ Handlers receive a context typed from middleware plus the action schema:
|
|
|
133
197
|
- `GET` handlers receive `ctx.path`, `ctx.query`, and `ctx.headers`
|
|
134
198
|
- mutation handlers receive `ctx.path`, `ctx.body`, and `ctx.headers`
|
|
135
199
|
- handlers may return a plain JSON-serializable value or a `Response`
|
|
200
|
+
- response-map handlers can return a default success value directly or use
|
|
201
|
+
`ctx.success(status, body)` and `ctx.error(status, body)`
|
|
202
|
+
- `ndjson.$type<T>()` handlers return an `Iterable<T>` or `AsyncIterable<T>`
|
|
203
|
+
unless they return a custom `Response`
|
|
136
204
|
- plain values are returned with `Response.json(value)`
|
|
205
|
+
- NDJSON iterables are returned as `application/x-ndjson` streams
|
|
137
206
|
- return a `Response` when you need custom status, headers, or body handling
|
|
138
207
|
|
|
139
208
|
`basePath` is prepended to route tree paths, `debug` adds matched-route debug
|
|
@@ -148,6 +217,10 @@ requests with an `Origin` header.
|
|
|
148
217
|
request factory contains the full path you want to call
|
|
149
218
|
- `client.json(action.request(args))` for parsed JSON and default non-2xx
|
|
150
219
|
throwing
|
|
220
|
+
- response-map support for generated client action functions, returning
|
|
221
|
+
`[error, value, status]` tuples for declared statuses
|
|
222
|
+
- response plugin support for generated client action functions, such as
|
|
223
|
+
`ndjson.clientPlugin` for NDJSON response streams
|
|
151
224
|
- a client tree that mirrors `routes`, with action functions such as
|
|
152
225
|
`client.profiles.get(args)` when `routes` is supplied
|
|
153
226
|
|
|
@@ -167,14 +240,18 @@ runtimes.
|
|
|
167
240
|
## Lifecycle
|
|
168
241
|
|
|
169
242
|
1. Define shared HTTP actions/resources with `rouzer/http` and Zod schemas.
|
|
170
|
-
2. Attach that route tree to a server with `createRouter().use(routes, handlers)
|
|
171
|
-
|
|
243
|
+
2. Attach that route tree to a server with `createRouter().use(routes, handlers)`
|
|
244
|
+
or `createRouter({ plugins }).use(routes, handlers)` when response plugins
|
|
245
|
+
are needed.
|
|
246
|
+
3. Create a client with the same route tree, plus matching client response
|
|
247
|
+
plugins when needed.
|
|
172
248
|
4. Client action calls validate `path`, `query`, `body`, and `headers` before
|
|
173
249
|
`fetch`.
|
|
174
250
|
5. The router matches the request, validates the matched inputs, and calls the
|
|
175
251
|
handler.
|
|
176
|
-
6. Plain handler results become JSON responses
|
|
177
|
-
|
|
252
|
+
6. Plain handler results become JSON responses, response-map helpers choose
|
|
253
|
+
declared statuses, plugin handler results become plugin-encoded responses, and
|
|
254
|
+
explicit `Response` objects pass through unchanged.
|
|
178
255
|
|
|
179
256
|
On the server, `path`, `query`, and `headers` values originate as strings. Rouzer
|
|
180
257
|
coerces Zod `number` schemas with `Number(value)` and Zod `boolean` schemas from
|
|
@@ -214,6 +291,110 @@ const json = await client.json(
|
|
|
214
291
|
)
|
|
215
292
|
```
|
|
216
293
|
|
|
294
|
+
Response maps and response plugins are applied by generated client action
|
|
295
|
+
functions. For longhand calls to mapped or plugin-backed routes, use
|
|
296
|
+
`client.request(...)` for the raw `Response` and decode the response yourself.
|
|
297
|
+
|
|
298
|
+
### Handle declared error responses
|
|
299
|
+
|
|
300
|
+
Use `$error<T>()` inside a response map when an error status is part of the route
|
|
301
|
+
contract:
|
|
302
|
+
|
|
303
|
+
```ts
|
|
304
|
+
import { $error, $type, createClient, createRouter } from 'rouzer'
|
|
305
|
+
import * as http from 'rouzer/http'
|
|
306
|
+
|
|
307
|
+
type User = { id: string; name: string }
|
|
308
|
+
type NotFound = { code: 'NOT_FOUND'; message: string }
|
|
309
|
+
|
|
310
|
+
export const getUser = http.get('users/:id', {
|
|
311
|
+
response: {
|
|
312
|
+
200: $type<User>(),
|
|
313
|
+
404: $error<NotFound>(),
|
|
314
|
+
},
|
|
315
|
+
})
|
|
316
|
+
export const routes = { getUser }
|
|
317
|
+
|
|
318
|
+
createRouter().use(routes, {
|
|
319
|
+
getUser(ctx) {
|
|
320
|
+
if (ctx.path.id === 'missing') {
|
|
321
|
+
return ctx.error(404, {
|
|
322
|
+
code: 'NOT_FOUND',
|
|
323
|
+
message: 'User not found',
|
|
324
|
+
})
|
|
325
|
+
}
|
|
326
|
+
return { id: ctx.path.id, name: 'Ada' }
|
|
327
|
+
},
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
const client = createClient({
|
|
331
|
+
baseURL: 'https://example.com/api/',
|
|
332
|
+
routes,
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
const [error, user, status] = await client.getUser({
|
|
336
|
+
path: { id: 'missing' },
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
if (status === 404) {
|
|
340
|
+
console.log(error.message)
|
|
341
|
+
} else {
|
|
342
|
+
console.log(user.name)
|
|
343
|
+
}
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
A complete runnable version lives in
|
|
347
|
+
[`examples/error-responses.ts`](../examples/error-responses.ts).
|
|
348
|
+
|
|
349
|
+
When a response map declares multiple success statuses, return a plain value for
|
|
350
|
+
the default success status or use `ctx.success(status, body)` to choose a
|
|
351
|
+
specific declared success status.
|
|
352
|
+
|
|
353
|
+
### Stream newline-delimited JSON
|
|
354
|
+
|
|
355
|
+
Use `ndjson.$type<T>()` when a handler should produce a sequence of JSON values
|
|
356
|
+
without buffering the whole response:
|
|
357
|
+
|
|
358
|
+
```ts
|
|
359
|
+
import { createClient, createRouter } from 'rouzer'
|
|
360
|
+
import * as http from 'rouzer/http'
|
|
361
|
+
import * as ndjson from 'rouzer/ndjson'
|
|
362
|
+
|
|
363
|
+
export const events = http.get('events', {
|
|
364
|
+
response: ndjson.$type<{ id: number; message: string }>(),
|
|
365
|
+
})
|
|
366
|
+
export const routes = { events }
|
|
367
|
+
|
|
368
|
+
createRouter({ plugins: [ndjson.routerPlugin] }).use(routes, {
|
|
369
|
+
async *events() {
|
|
370
|
+
yield { id: 1, message: 'ready' }
|
|
371
|
+
yield { id: 2, message: 'done' }
|
|
372
|
+
},
|
|
373
|
+
})
|
|
374
|
+
|
|
375
|
+
const client = createClient({
|
|
376
|
+
baseURL: 'https://example.com/api/',
|
|
377
|
+
routes,
|
|
378
|
+
plugins: [ndjson.clientPlugin],
|
|
379
|
+
})
|
|
380
|
+
for await (const event of await client.events()) {
|
|
381
|
+
console.log(event.message)
|
|
382
|
+
}
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
A complete runnable version lives in
|
|
386
|
+
[`examples/ndjson-stream.ts`](../examples/ndjson-stream.ts).
|
|
387
|
+
|
|
388
|
+
Rouzer's decoder accepts `\n` and `\r\n`, handles UTF-8 chunk boundaries, and
|
|
389
|
+
throws a `SyntaxError` with a line number for malformed JSON. If a consumer stops
|
|
390
|
+
reading early, the response body is cancelled.
|
|
391
|
+
|
|
392
|
+
Rouzer does not convert handler or generator failures into extra NDJSON items. If
|
|
393
|
+
an async generator throws after the response starts, the response stream errors
|
|
394
|
+
and the client's `for await` loop throws. Model application-level stream errors
|
|
395
|
+
as part of your item type, for example `{ type: 'error'; message: string }`, when
|
|
396
|
+
clients should receive them as data.
|
|
397
|
+
|
|
217
398
|
### Group resource actions
|
|
218
399
|
|
|
219
400
|
Use resources when the public API reads better as a tree or when actions share
|
|
@@ -239,17 +420,18 @@ custom headers. Return a plain value for the default `Response.json(value)` path
|
|
|
239
420
|
|
|
240
421
|
### Customize JSON errors
|
|
241
422
|
|
|
242
|
-
By default, `client.json(...)`
|
|
423
|
+
By default, `client.json(...)` and generated client action functions throw for
|
|
424
|
+
non-2xx responses that are not declared in a response map. If the response body
|
|
243
425
|
is JSON, its properties are copied onto the thrown `Error`.
|
|
244
426
|
|
|
245
|
-
`onJsonError` can override that behavior. Its return value is returned from
|
|
246
|
-
|
|
247
|
-
|
|
427
|
+
`onJsonError` can override that behavior. Its return value is returned from the
|
|
428
|
+
response helper as-is; Rouzer does not automatically parse a returned `Response`
|
|
429
|
+
from `onJsonError`.
|
|
248
430
|
|
|
249
|
-
###
|
|
431
|
+
### v2->v3 migration
|
|
250
432
|
|
|
251
433
|
Rouzer now uses action/resource route trees for router registration and client
|
|
252
|
-
shorthands.
|
|
434
|
+
shorthands. In the v2->v3 migration, a method-map route such as this:
|
|
253
435
|
|
|
254
436
|
```ts
|
|
255
437
|
export const profileRoute = route('profiles/:id', {
|
|
@@ -307,6 +489,11 @@ await client.profiles.update({
|
|
|
307
489
|
only when string params are sufficient.
|
|
308
490
|
- Use `response: $type<T>()` for JSON endpoints that should have typed client
|
|
309
491
|
action functions.
|
|
492
|
+
- Use response maps with `$error<T>()` when callers should handle declared error
|
|
493
|
+
statuses as typed data instead of exceptions.
|
|
494
|
+
- Use `response: ndjson.$type<T>()` plus `ndjson.routerPlugin` and
|
|
495
|
+
`ndjson.clientPlugin` for response streams where each line is a JSON value and
|
|
496
|
+
the client should consume an `AsyncIterable<T>`.
|
|
310
497
|
- Name actions after domain operations (`get`, `list`, `update`, `archive`) and
|
|
311
498
|
let `http.get/post/put/patch/delete` own the transport method.
|
|
312
499
|
- Set `content-type: application/json` yourself when your server or middleware
|
|
@@ -314,7 +501,15 @@ await client.profiles.update({
|
|
|
314
501
|
|
|
315
502
|
## Constraints and gotchas
|
|
316
503
|
|
|
317
|
-
- `$type<T>()`
|
|
504
|
+
- `$type<T>()`, `$error<T>()`, and `ndjson.$type<T>()` are compile-time-only type
|
|
505
|
+
contracts. Rouzer does not re-validate handler return values at the server
|
|
506
|
+
boundary.
|
|
507
|
+
- NDJSON support is for response streams; request bodies still use the existing
|
|
508
|
+
JSON body schema path.
|
|
509
|
+
- Declared `$error<T>()` responses are JSON responses. Use a custom `Response`
|
|
510
|
+
for non-JSON error payloads.
|
|
511
|
+
- Routes that use a response plugin fail fast if the matching client or router
|
|
512
|
+
plugin is not registered.
|
|
318
513
|
- Pathname route patterns expect an absolute client `baseURL`.
|
|
319
514
|
- Resource and action keys are API names only; paths come from the pattern
|
|
320
515
|
strings passed to `http.resource(...)` and action helpers.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { HattipHandler } from '@hattip/core'
|
|
2
|
+
import { $error, $type, createClient, createRouter } from 'rouzer'
|
|
3
|
+
import * as http from 'rouzer/http'
|
|
4
|
+
|
|
5
|
+
type User = {
|
|
6
|
+
id: string
|
|
7
|
+
name: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
type AuthError = {
|
|
11
|
+
code: 'UNAUTHORIZED'
|
|
12
|
+
message: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type NotFoundError = {
|
|
16
|
+
code: 'NOT_FOUND'
|
|
17
|
+
message: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const getUser = http.get('users/:id', {
|
|
21
|
+
response: {
|
|
22
|
+
200: $type<User>(),
|
|
23
|
+
201: $type<User>(),
|
|
24
|
+
401: $error<AuthError>(),
|
|
25
|
+
404: $error<NotFoundError>(),
|
|
26
|
+
},
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
export const routes = { getUser }
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Tiny Hattip adapter used only to keep this example self-contained. Real apps
|
|
33
|
+
* mount the handler with a Hattip adapter for their runtime.
|
|
34
|
+
*/
|
|
35
|
+
function createLocalFetch(handler: HattipHandler): typeof fetch {
|
|
36
|
+
return async (input, init) => {
|
|
37
|
+
const request = new Request(input, init)
|
|
38
|
+
const response = await handler({
|
|
39
|
+
request,
|
|
40
|
+
ip: '127.0.0.1',
|
|
41
|
+
platform: undefined,
|
|
42
|
+
env() {
|
|
43
|
+
return undefined
|
|
44
|
+
},
|
|
45
|
+
passThrough() {},
|
|
46
|
+
waitUntil(promise) {
|
|
47
|
+
void promise
|
|
48
|
+
},
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
return response ?? new Response(null, { status: 404 })
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function runErrorResponsesExample() {
|
|
56
|
+
const users = new Map([['42', { id: '42', name: 'Ada' }]])
|
|
57
|
+
|
|
58
|
+
const handler = createRouter({ basePath: 'api/' }).use(routes, {
|
|
59
|
+
getUser(ctx) {
|
|
60
|
+
if (ctx.path.id === 'unauthorized') {
|
|
61
|
+
return ctx.error(401, {
|
|
62
|
+
code: 'UNAUTHORIZED',
|
|
63
|
+
message: 'Login required',
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (ctx.path.id === 'created') {
|
|
68
|
+
return ctx.success(201, {
|
|
69
|
+
id: 'created',
|
|
70
|
+
name: 'Grace',
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const user = users.get(ctx.path.id)
|
|
75
|
+
if (!user) {
|
|
76
|
+
return ctx.error(404, {
|
|
77
|
+
code: 'NOT_FOUND',
|
|
78
|
+
message: 'User not found',
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return user
|
|
83
|
+
},
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
const client = createClient({
|
|
87
|
+
baseURL: 'https://example.test/api/',
|
|
88
|
+
routes,
|
|
89
|
+
fetch: createLocalFetch(handler),
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
const found = await client.getUser({ path: { id: '42' } })
|
|
93
|
+
const created = await client.getUser({ path: { id: 'created' } })
|
|
94
|
+
const missing = await client.getUser({ path: { id: 'missing' } })
|
|
95
|
+
const unauthorized = await client.getUser({ path: { id: 'unauthorized' } })
|
|
96
|
+
|
|
97
|
+
return { found, created, missing, unauthorized }
|
|
98
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { HattipHandler } from '@hattip/core'
|
|
2
|
+
import { createClient, createRouter } from 'rouzer'
|
|
3
|
+
import * as http from 'rouzer/http'
|
|
4
|
+
import * as ndjson from 'rouzer/ndjson'
|
|
5
|
+
|
|
6
|
+
type Event = {
|
|
7
|
+
id: number
|
|
8
|
+
message: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const events = http.get('events', {
|
|
12
|
+
response: ndjson.$type<Event>(),
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
export const routes = { events }
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Tiny Hattip adapter used only to keep this example self-contained. Real apps
|
|
19
|
+
* mount the handler with a Hattip adapter for their runtime.
|
|
20
|
+
*/
|
|
21
|
+
function createLocalFetch(handler: HattipHandler): typeof fetch {
|
|
22
|
+
return async (input, init) => {
|
|
23
|
+
const request = new Request(input, init)
|
|
24
|
+
const response = await handler({
|
|
25
|
+
request,
|
|
26
|
+
ip: '127.0.0.1',
|
|
27
|
+
platform: undefined,
|
|
28
|
+
env() {
|
|
29
|
+
return undefined
|
|
30
|
+
},
|
|
31
|
+
passThrough() {},
|
|
32
|
+
waitUntil(promise) {
|
|
33
|
+
void promise
|
|
34
|
+
},
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
return response ?? new Response(null, { status: 404 })
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function collect<T>(source: AsyncIterable<T>) {
|
|
42
|
+
const values: T[] = []
|
|
43
|
+
for await (const value of source) {
|
|
44
|
+
values.push(value)
|
|
45
|
+
}
|
|
46
|
+
return values
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function runNdjsonStreamExample() {
|
|
50
|
+
const handler = createRouter({
|
|
51
|
+
basePath: 'api/',
|
|
52
|
+
plugins: [ndjson.routerPlugin],
|
|
53
|
+
}).use(routes, {
|
|
54
|
+
async *events() {
|
|
55
|
+
yield { id: 1, message: 'ready' }
|
|
56
|
+
yield { id: 2, message: 'done' }
|
|
57
|
+
},
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
const client = createClient({
|
|
61
|
+
baseURL: 'https://example.test/api/',
|
|
62
|
+
routes,
|
|
63
|
+
plugins: [ndjson.clientPlugin],
|
|
64
|
+
fetch: createLocalFetch(handler),
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
return collect(await client.events())
|
|
68
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rouzer",
|
|
3
|
-
"version": "3.0
|
|
3
|
+
"version": "3.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -10,6 +10,10 @@
|
|
|
10
10
|
"./http": {
|
|
11
11
|
"types": "./dist/http.d.ts",
|
|
12
12
|
"import": "./dist/http.js"
|
|
13
|
+
},
|
|
14
|
+
"./ndjson": {
|
|
15
|
+
"types": "./dist/ndjson.d.ts",
|
|
16
|
+
"import": "./dist/ndjson.js"
|
|
13
17
|
}
|
|
14
18
|
},
|
|
15
19
|
"peerDependencies": {
|
|
@@ -48,6 +52,7 @@
|
|
|
48
52
|
],
|
|
49
53
|
"scripts": {
|
|
50
54
|
"build": "rm -rf dist && tsgo -b tsconfig.json",
|
|
55
|
+
"format": "prettier --write src test",
|
|
51
56
|
"test": "vitest run"
|
|
52
57
|
}
|
|
53
58
|
}
|